From d38a1cec09bf6a973d521fd42fa54b7affe352af Mon Sep 17 00:00:00 2001 From: Alessandro Dell'Oste Date: Tue, 25 Jun 2024 11:06:08 +0200 Subject: [PATCH 1/9] Remove DSGradientScroll component --- .../design-system/core/DSGradientScroll.tsx | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 ts/features/design-system/core/DSGradientScroll.tsx diff --git a/ts/features/design-system/core/DSGradientScroll.tsx b/ts/features/design-system/core/DSGradientScroll.tsx deleted file mode 100644 index 4c0e4bfaaff..00000000000 --- a/ts/features/design-system/core/DSGradientScroll.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react"; -import { Alert, View } from "react-native"; -import { - ButtonOutline, - GradientScrollView, - IOColors -} from "@pagopa/io-app-design-system"; -import { H2 } from "../../../components/core/typography/H2"; -import { Body } from "../../../components/core/typography/Body"; - -export const DSGradientScroll = () => ( - - Alert.alert("Primary action pressed! (⁠⁠ꈍ⁠ᴗ⁠ꈍ⁠)") - }} - > -

Start

- {[...Array(50)].map((_el, i) => ( - Repeated text - ))} - Alert.alert("Test button")} - /> - {[...Array(2)].map((_el, i) => ( - Repeated text - ))} -

End

-
-
-); From 45438f08c368c44941f88d12fa81cb5c99b69c79 Mon Sep 17 00:00:00 2001 From: Alessandro Dell'Oste Date: Tue, 25 Jun 2024 11:08:36 +0200 Subject: [PATCH 2/9] Update email notification preferences --- ...ckEmailNotificationPreferencesSaga.test.ts | 59 +------------------ .../checkEmailNotificationPreferencesSaga.ts | 53 +---------------- ts/screens/profile/EmailForwardingScreen.tsx | 37 ++---------- 3 files changed, 8 insertions(+), 141 deletions(-) diff --git a/ts/sagas/startup/__tests__/checkEmailNotificationPreferencesSaga.test.ts b/ts/sagas/startup/__tests__/checkEmailNotificationPreferencesSaga.test.ts index 47f3956bcac..2cf5141d0bf 100644 --- a/ts/sagas/startup/__tests__/checkEmailNotificationPreferencesSaga.test.ts +++ b/ts/sagas/startup/__tests__/checkEmailNotificationPreferencesSaga.test.ts @@ -1,14 +1,7 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { testSaga } from "redux-saga-test-plan"; -import { customEmailChannelSetEnabled } from "../../../store/actions/persistedPreferences"; -import { visibleServicesSelector } from "../../../store/reducers/entities/services/visibleServices"; import { isCustomEmailChannelEnabledSelector } from "../../../store/reducers/persistedPreferences"; -import { profileSelector } from "../../../store/reducers/profile"; -import { - checkEmailNotificationPreferencesSaga, - emailNotificationPreferencesSaga, - watchEmailNotificationPreferencesSaga -} from "../checkEmailNotificationPreferencesSaga"; +import { watchEmailNotificationPreferencesSaga } from "../checkEmailNotificationPreferencesSaga"; describe("watchEmailNotificationPreferencesSaga", () => { it("if the store has information about user preferences the saga should end", () => { @@ -23,54 +16,6 @@ describe("watchEmailNotificationPreferencesSaga", () => { testSaga(watchEmailNotificationPreferencesSaga) .next() .select(isCustomEmailChannelEnabledSelector) - .next(pot.none) // have no information about user preferences - .fork(checkEmailNotificationPreferencesSaga) - .next(); - }); -}); - -describe("emailNotificationPreferencesSaga", () => { - it("if profile has is_email_enabled to false, isCustomEmailChannelEnabled should be false", () => { - testSaga(emailNotificationPreferencesSaga) - .next() - .select(profileSelector) - .next(pot.some({ is_email_enabled: false })) // profile - .select(visibleServicesSelector) - .next(pot.some([])) // visible services - .put(customEmailChannelSetEnabled(false)) - .next() - .isDone(); - }); - - it("if profile has is_email_enabled to true but not blocked email, isCustomEmailChannelEnabled should be false", () => { - testSaga(emailNotificationPreferencesSaga) - .next() - .select(profileSelector) - .next(pot.some({ is_email_enabled: true, blocked_inbox_or_channels: {} })) // profile - .select(visibleServicesSelector) - .next(pot.some([])) // visible services - .put(customEmailChannelSetEnabled(false)) - .next() - .isDone(); - }); - - it("if profile has is_email_enabled to true and blocked emails, isCustomEmailChannelEnabled should be true", () => { - testSaga(emailNotificationPreferencesSaga) - .next() - .select(profileSelector) - .next( - pot.some({ - is_email_enabled: true, - blocked_inbox_or_channels: { - service1: ["EMAIL"], - service2: ["EMAIL"] - } - }) - ) // profile - .select(visibleServicesSelector) - .next(pot.some([{ service_id: "service1" }])) // visible services - .put(customEmailChannelSetEnabled(true)) - .next() - .isDone(); + .next(pot.none); // have no information about user preferences }); }); diff --git a/ts/sagas/startup/checkEmailNotificationPreferencesSaga.ts b/ts/sagas/startup/checkEmailNotificationPreferencesSaga.ts index ccfaadacbfc..d0446ca5f37 100644 --- a/ts/sagas/startup/checkEmailNotificationPreferencesSaga.ts +++ b/ts/sagas/startup/checkEmailNotificationPreferencesSaga.ts @@ -1,4 +1,3 @@ -import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { SagaIterator } from "redux-saga"; import { @@ -10,17 +9,9 @@ import { takeEvery } from "typed-redux-saga/macro"; import { getType } from "typesafe-actions"; -import { pipe } from "fp-ts/lib/function"; -import { BlockedInboxOrChannels } from "../../../definitions/backend/BlockedInboxOrChannels"; import { customEmailChannelSetEnabled } from "../../store/actions/persistedPreferences"; import { profileLoadSuccess } from "../../store/actions/profile"; -import { loadVisibleServices } from "../../store/actions/services"; -import { - visibleServicesSelector, - VisibleServicesState -} from "../../store/reducers/entities/services/visibleServices"; import { isCustomEmailChannelEnabledSelector } from "../../store/reducers/persistedPreferences"; -import { profileSelector, ProfileState } from "../../store/reducers/profile"; import { ReduxSagaEffect } from "../../types/utils"; /** @@ -49,51 +40,11 @@ export function* watchEmailNotificationPreferencesSaga(): Generator< export function* checkEmailNotificationPreferencesSaga(): SagaIterator { yield* takeEvery( - [getType(profileLoadSuccess), getType(loadVisibleServices.success)], + getType(profileLoadSuccess), emailNotificationPreferencesSaga ); } export function* emailNotificationPreferencesSaga(): SagaIterator { - const potProfile: ProfileState = yield* select(profileSelector); - const potVisibleServices: VisibleServicesState = yield* select( - visibleServicesSelector - ); - /** - * if we have a visible services and a profile (with a defined blocked_inbox_or_channels) - * check if there is at least a service with EMAIL channel blocked. This means user has done - * a custom choice - */ - const potCustomEmailChannelEnabled = pot.map( - potVisibleServices, - visibleService => { - const maybeSomeEmailBlocked = pot.map(potProfile, profile => { - // custom email could be true only if profile.is_email_enabled === true - // and the user made some optin on email channels - if (profile.is_email_enabled === false) { - return false; - } - const blockedChannels: BlockedInboxOrChannels = pipe( - profile.blocked_inbox_or_channels, - O.fromNullable, - O.getOrElse(() => ({})) - ); - return ( - visibleService.findIndex( - service => - blockedChannels[service.service_id] && - blockedChannels[service.service_id].indexOf("EMAIL") !== -1 - ) !== -1 - ); - }); - return pot.getOrElse(maybeSomeEmailBlocked, false); - } - ); - // If the email notification for visible services are partially disabled - // (only for some services), the customization is enabled - yield* put( - customEmailChannelSetEnabled( - pot.getOrElse(potCustomEmailChannelEnabled, false) - ) - ); + yield* put(customEmailChannelSetEnabled(false)); } diff --git a/ts/screens/profile/EmailForwardingScreen.tsx b/ts/screens/profile/EmailForwardingScreen.tsx index d23f67986d5..16c795bb2a7 100644 --- a/ts/screens/profile/EmailForwardingScreen.tsx +++ b/ts/screens/profile/EmailForwardingScreen.tsx @@ -24,17 +24,11 @@ import { ProfileParamsList } from "../../navigation/params/ProfileParamsList"; import { customEmailChannelSetEnabled } from "../../store/actions/persistedPreferences"; import { profileUpsert } from "../../store/actions/profile"; import { - VisibleServicesState, - visibleServicesSelector -} from "../../store/reducers/entities/services/visibleServices"; -import { - ProfileState, isEmailEnabledSelector, profileEmailSelector, profileSelector } from "../../store/reducers/profile"; import { GlobalState } from "../../store/reducers/types"; -import { getProfileChannelsforServicesList } from "../../utils/profile"; import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; type OwnProps = { @@ -167,10 +161,7 @@ class EmailForwardingScreenClass extends React.Component { this.setState( { isCustomChannelEnabledChoice: false, isLoading: true }, () => { - this.props.disableOrEnableAllEmailNotifications( - this.props.visibleServicesId, - this.props.potProfile - ); + this.props.disableOrEnableAllEmailNotifications(); } ); } @@ -209,15 +200,6 @@ class EmailForwardingScreenClass extends React.Component { } const mapStateToProps = (state: GlobalState) => { - const potVisibleServices: VisibleServicesState = - visibleServicesSelector(state); - const visibleServicesId = pot.getOrElse( - pot.map(potVisibleServices, services => - services.map(service => service.service_id) - ), - [] - ); - const potProfile = profileSelector(state); // const potIsCustomEmailChannelEnabled = isCustomEmailChannelEnabledSelector( // state @@ -240,29 +222,18 @@ const mapStateToProps = (state: GlobalState) => { isLoading: pot.isLoading(potProfile) || pot.isUpdating(potProfile), isEmailEnabled: isEmailEnabledSelector(state), isCustomEmailChannelEnabled, - visibleServicesId, userEmail }; }; const mapDispatchToProps = (dispatch: Dispatch) => ({ - disableOrEnableAllEmailNotifications: ( - servicesId: ReadonlyArray, - profile: ProfileState - ) => { - const newBlockedChannels = getProfileChannelsforServicesList( - servicesId, - profile, - true, - "EMAIL" - ); + disableOrEnableAllEmailNotifications: () => dispatch( profileUpsert.request({ - blocked_inbox_or_channels: newBlockedChannels, + blocked_inbox_or_channels: {}, is_email_enabled: true }) - ); - }, + ), setCustomEmailChannelEnabled: (customEmailChannelEnabled: boolean) => { dispatch(customEmailChannelSetEnabled(customEmailChannelEnabled)); }, From 81058afe8ba8bc7601eca54719242c17c1a60a07 Mon Sep 17 00:00:00 2001 From: Alessandro Dell'Oste Date: Tue, 25 Jun 2024 11:09:29 +0200 Subject: [PATCH 3/9] remove old implementation of services --- .env.local | 4 - .env.production | 4 - ts/api/backend.ts | 13 - .../__snapshots__/persistedStore.test.ts.snap | 14 - ts/boot/configureStoreAndPersistor.ts | 11 +- ts/components/screens/BadgeComponent.tsx | 2 +- .../screens/SectionHeaderComponent.tsx | 1 - ts/components/search/SearchButton.tsx | 6 +- .../PreferenceToggleRow.tsx | 89 --- .../ContactPreferencesToggles.test.tsx | 228 -------- .../__test__/PreferenceToggleRow.test.tsx | 95 --- .../PreferenceToggleRow.test.tsx.snap | 83 --- .../ContactPreferencesToggles/index.tsx | 232 -------- ts/components/services/LinkRow.tsx | 39 -- .../services/LocalServicesWebView.tsx | 175 ------ ts/components/services/NewServiceListItem.tsx | 41 -- ts/components/services/SectionHeader.tsx | 36 -- ts/components/services/ServiceList.tsx | 134 ----- .../ServiceMetadata/InformationRow.tsx | 63 -- .../__tests__/InformationRow.test.tsx | 20 - .../__tests__/ServiceMetadata.test.tsx | 329 ----------- .../InformationRow.test.tsx.snap | 112 ---- .../services/ServiceMetadata/index.tsx | 151 ----- ts/components/services/ServicesSearch.tsx | 162 ------ .../services/ServicesSectionsList.tsx | 81 --- ts/components/services/ServicesTab.tsx | 45 -- .../LegacySpecialServicesCTA.tsx | 118 ---- ts/components/services/TosAndPrivacyBox.tsx | 29 - .../services/__tests__/LinkRow.test.tsx | 13 - .../services/__tests__/SectionHeader.test.tsx | 13 - .../__tests__/TosAndPrivacyBox.test.tsx | 104 ---- .../__snapshots__/LinkRow.test.tsx.snap | 55 -- .../__snapshots__/SectionHeader.test.tsx.snap | 110 ---- ts/config.ts | 15 - .../bonus/cgn/components/CgnServiceCTA.tsx | 8 +- .../cgn/components/LegacyCgnServiceCTA.tsx | 6 +- .../common/screens/AvailableBonusScreen.tsx | 12 +- .../bonus/common/store/selectors/index.ts | 2 +- ts/features/fci/hooks/useFciCheckService.tsx | 6 +- .../screens/valid/FciQtspClausesScreen.tsx | 6 +- .../fims/components/FimsSuccessBody.tsx | 2 +- .../screens/PDNDPrerequisitesScreen.tsx | 2 +- .../Home/__tests__/homeUtils.test.ts | 78 ++- .../messages/components/Home/homeUtils.ts | 2 +- .../MessageDetail/MessageDetailsFooter.tsx | 2 +- .../MessageDetail/MessageDetailsHeader.tsx | 4 +- .../__test__/handleLoadMessageData.test.ts | 2 +- .../messages/saga/handleLoadMessageData.ts | 2 +- .../messages/screens/MessageDetailsScreen.tsx | 2 +- .../legacy/LegacyMessageDetailScreen.tsx | 2 +- .../pn/components/LegacyServiceCTA.tsx | 8 +- .../pn/components/MessageBottomMenu.tsx | 2 +- .../pn/components/MessageDetailsHeader.tsx | 2 +- ts/features/pn/components/ServiceCTA.tsx | 8 +- .../pn/screens/LegacyMessageDetailsScreen.tsx | 6 +- ts/features/pn/store/sagas/watchPnSaga.ts | 6 +- .../services/common/store/reducers/index.ts | 5 + ts/features/services/common/utils/index.ts | 21 + .../components/ServiceDetailsMetadata.tsx | 2 +- .../components/ServiceDetailsPreferences.tsx | 4 +- .../ServiceDetailsTosAndPrivacy.tsx | 2 +- .../__tests__/handleServiceDetails.test.ts | 6 - .../details/saga/handleServiceDetails.ts | 16 - .../saga/handleUpsertServicePreference.ts | 16 +- .../details/screens/ServiceDetailsScreen.tsx | 4 +- .../__tests__/servicePreference.test.ts | 41 +- .../reducers/__tests__/servicesById.test.ts | 45 +- .../services/details/store/reducers/index.ts | 214 +++++++ .../store/reducers/servicePreference.ts | 90 --- .../details/store/reducers/servicesById.ts | 137 ----- .../component/card/FeaturedCardCarousel.tsx | 7 +- ts/navigation/ServicesHomeTabNavigator.tsx | 52 -- ts/navigation/TabNavigator.tsx | 2 - .../handleFirstVisibleServiceLoadSaga.test.ts | 40 -- .../handleOrganizationNameUpdateSaga.test.ts | 74 --- .../handleServiceReadabilitySaga.test.ts | 27 - .../__tests__/refreshStoredServices.test.ts | 73 --- .../handleFirstVisibleServiceLoadSaga.ts | 24 - .../handleOrganizationNameUpdateSaga.ts | 52 -- .../services/handleServiceReadabilitySaga.ts | 20 - ts/sagas/services/refreshStoredServices.ts | 44 -- .../services/removeUnusedStoredServices.ts | 60 -- ts/sagas/services/watchLoadServicesSaga.ts | 39 -- .../loadVisibleServicesHandler.test.ts | 74 --- .../loadServiceDetailRequestHandler.ts | 190 ------ .../startup/loadVisibleServicesHandler.ts | 54 -- .../services/LegacyServiceDetailsScreen.tsx | 221 ------- ts/screens/services/ServicesHomeScreen.tsx | 541 ------------------ ts/screens/services/ServicesLocalScreen.tsx | 43 -- .../services/ServicesNationalScreen.tsx | 66 --- .../__tests__/ServiceDetailsScreen.test.tsx | 80 --- ts/store/actions/navigation.ts | 10 - ts/store/actions/search.ts | 5 - ts/store/actions/services/index.ts | 71 --- ts/store/actions/types.ts | 4 +- ts/store/reducers/entities/index.ts | 19 +- .../entities/services/__tests__/index.test.ts | 337 ----------- .../__tests__/readStateByServiceId.test.ts | 53 -- .../servicesByOrganizationFiscalCode.test.ts | 35 -- .../entities/services/firstServicesLoading.ts | 34 -- ts/store/reducers/entities/services/index.ts | 426 -------------- .../entities/services/readStateByServiceId.ts | 52 -- .../servicesByOrganizationFiscalCode.ts | 95 --- .../entities/services/transformers.ts | 2 +- .../entities/services/visibleServices.ts | 48 -- ts/store/reducers/index.ts | 1 - ts/store/reducers/search.ts | 15 +- ts/types/ServicesWebviewParams.ts | 13 - ts/types/WebviewMessage.ts | 2 +- .../__tests__/ServicesWebviewParams.test.ts | 38 -- ts/utils/analytics.ts | 15 - ts/utils/organizations.ts | 21 - ts/utils/profile.ts | 145 ----- ts/utils/services.ts | 57 -- 114 files changed, 395 insertions(+), 6286 deletions(-) delete mode 100644 ts/components/services/ContactPreferencesToggles/PreferenceToggleRow.tsx delete mode 100644 ts/components/services/ContactPreferencesToggles/__test__/ContactPreferencesToggles.test.tsx delete mode 100644 ts/components/services/ContactPreferencesToggles/__test__/PreferenceToggleRow.test.tsx delete mode 100644 ts/components/services/ContactPreferencesToggles/__test__/__snapshots__/PreferenceToggleRow.test.tsx.snap delete mode 100644 ts/components/services/ContactPreferencesToggles/index.tsx delete mode 100644 ts/components/services/LinkRow.tsx delete mode 100644 ts/components/services/LocalServicesWebView.tsx delete mode 100644 ts/components/services/NewServiceListItem.tsx delete mode 100644 ts/components/services/SectionHeader.tsx delete mode 100644 ts/components/services/ServiceList.tsx delete mode 100644 ts/components/services/ServiceMetadata/InformationRow.tsx delete mode 100644 ts/components/services/ServiceMetadata/__tests__/InformationRow.test.tsx delete mode 100644 ts/components/services/ServiceMetadata/__tests__/ServiceMetadata.test.tsx delete mode 100644 ts/components/services/ServiceMetadata/__tests__/__snapshots__/InformationRow.test.tsx.snap delete mode 100644 ts/components/services/ServiceMetadata/index.tsx delete mode 100644 ts/components/services/ServicesSearch.tsx delete mode 100644 ts/components/services/ServicesSectionsList.tsx delete mode 100644 ts/components/services/ServicesTab.tsx delete mode 100644 ts/components/services/SpecialServices/LegacySpecialServicesCTA.tsx delete mode 100644 ts/components/services/TosAndPrivacyBox.tsx delete mode 100644 ts/components/services/__tests__/LinkRow.test.tsx delete mode 100644 ts/components/services/__tests__/SectionHeader.test.tsx delete mode 100644 ts/components/services/__tests__/TosAndPrivacyBox.test.tsx delete mode 100644 ts/components/services/__tests__/__snapshots__/LinkRow.test.tsx.snap delete mode 100644 ts/components/services/__tests__/__snapshots__/SectionHeader.test.tsx.snap create mode 100644 ts/features/services/details/store/reducers/index.ts delete mode 100644 ts/features/services/details/store/reducers/servicePreference.ts delete mode 100644 ts/features/services/details/store/reducers/servicesById.ts delete mode 100644 ts/navigation/ServicesHomeTabNavigator.tsx delete mode 100644 ts/sagas/services/__tests__/handleFirstVisibleServiceLoadSaga.test.ts delete mode 100644 ts/sagas/services/__tests__/handleOrganizationNameUpdateSaga.test.ts delete mode 100644 ts/sagas/services/__tests__/handleServiceReadabilitySaga.test.ts delete mode 100644 ts/sagas/services/__tests__/refreshStoredServices.test.ts delete mode 100644 ts/sagas/services/handleFirstVisibleServiceLoadSaga.ts delete mode 100644 ts/sagas/services/handleOrganizationNameUpdateSaga.ts delete mode 100644 ts/sagas/services/handleServiceReadabilitySaga.ts delete mode 100644 ts/sagas/services/refreshStoredServices.ts delete mode 100644 ts/sagas/services/removeUnusedStoredServices.ts delete mode 100644 ts/sagas/services/watchLoadServicesSaga.ts delete mode 100644 ts/sagas/startup/__tests__/loadVisibleServicesHandler.test.ts delete mode 100644 ts/sagas/startup/loadServiceDetailRequestHandler.ts delete mode 100644 ts/sagas/startup/loadVisibleServicesHandler.ts delete mode 100644 ts/screens/services/LegacyServiceDetailsScreen.tsx delete mode 100644 ts/screens/services/ServicesHomeScreen.tsx delete mode 100644 ts/screens/services/ServicesLocalScreen.tsx delete mode 100644 ts/screens/services/ServicesNationalScreen.tsx delete mode 100644 ts/screens/services/__tests__/ServiceDetailsScreen.test.tsx delete mode 100644 ts/store/actions/services/index.ts delete mode 100644 ts/store/reducers/entities/services/__tests__/index.test.ts delete mode 100644 ts/store/reducers/entities/services/__tests__/readStateByServiceId.test.ts delete mode 100644 ts/store/reducers/entities/services/__tests__/servicesByOrganizationFiscalCode.test.ts delete mode 100644 ts/store/reducers/entities/services/firstServicesLoading.ts delete mode 100644 ts/store/reducers/entities/services/index.ts delete mode 100644 ts/store/reducers/entities/services/readStateByServiceId.ts delete mode 100644 ts/store/reducers/entities/services/servicesByOrganizationFiscalCode.ts delete mode 100644 ts/store/reducers/entities/services/visibleServices.ts delete mode 100644 ts/types/ServicesWebviewParams.ts delete mode 100644 ts/types/__tests__/ServicesWebviewParams.test.ts delete mode 100644 ts/utils/organizations.ts delete mode 100644 ts/utils/services.ts diff --git a/.env.local b/.env.local index 9e3fd6931e9..09bc595fd18 100644 --- a/.env.local +++ b/.env.local @@ -20,8 +20,6 @@ FETCH_PAYMENT_MANAGER_TIMEOUT_MS=16000 FETCH_MAX_RETRIES=3 # number of workers to fetch message TOT_MESSAGE_FETCH_WORKERS=5 -# number of workers to fetch service -TOT_SERVICE_FETCH_WORKERS=5 # shuffle pin pad to proceed with the payment SHUFFLE_PINPAD_ON_PAYMENT=NO # Repository of app content @@ -41,8 +39,6 @@ PLAYGROUNDS_ENABLED=YES BONUS_API_URL_PREFIX='http://127.0.0.1:3000/bonus' BONUS_API_SIT_BASEURL='https://api-io.dev.cstar.pagopa.it' BONUS_API_UAT_BASEURL='https://api-io.uat.cstar.pagopa.it' -# local services web url -LOCAL_SERVICE_WEB_URL='http://127.0.0.1:3000/services_web_view' # EU Covid Certificate EU_COVID_CERT_ENABLED=YES # Zendesk configuration diff --git a/.env.production b/.env.production index f5c7a007cc5..4c4075ebff7 100644 --- a/.env.production +++ b/.env.production @@ -20,8 +20,6 @@ FETCH_PAYMENT_MANAGER_TIMEOUT_MS=16000 FETCH_MAX_RETRIES=3 # number of workers to fetch message TOT_MESSAGE_FETCH_WORKERS=5 -# number of workers to fetch service -TOT_SERVICE_FETCH_WORKERS=5 # shuffle pin pad to proceed with the payment SHUFFLE_PINPAD_ON_PAYMENT=NO # Repository of app content @@ -41,8 +39,6 @@ PLAYGROUNDS_ENABLED=YES BONUS_API_URL_PREFIX=https://api-io.cstar.pagopa.it BONUS_API_SIT_BASEURL='https://api-io.dev.cstar.pagopa.it' BONUS_API_UAT_BASEURL='https://api-io.uat.cstar.pagopa.it' -# local services web url -LOCAL_SERVICE_WEB_URL='https://io.italia.it/app-content/enti-servizi.html' # EU Covid Certificate EU_COVID_CERT_ENABLED=YES # Zendesk configuration diff --git a/ts/api/backend.ts b/ts/api/backend.ts index aa8259716c9..3894af40905 100644 --- a/ts/api/backend.ts +++ b/ts/api/backend.ts @@ -42,8 +42,6 @@ import { getUserMetadataDefaultDecoder, GetUserMetadataT, GetUserProfileT, - getVisibleServicesDefaultDecoder, - GetVisibleServicesT, StartEmailValidationProcessT, updateProfileDefaultDecoder, UpdateProfileT, @@ -181,14 +179,6 @@ export function BackendClient( response_decoder: upsertServicePreferencesDefaultDecoder() }; - const getVisibleServicesT: GetVisibleServicesT = { - method: "get", - url: () => "/api/v1/services", - query: _ => ({}), - headers: tokenHeaderProducer, - response_decoder: getVisibleServicesDefaultDecoder() - }; - const getMessagesT: GetUserMessagesT = { method: "get", url: _ => "/api/v1/messages", @@ -385,9 +375,6 @@ export function BackendClient( upsertServicePreference: withBearerToken( createFetchRequestForApi(upsertServicePreferenceT, options) ), - getVisibleServices: withBearerToken( - createFetchRequestForApi(getVisibleServicesT, options) - ), getMessages: withBearerToken( createFetchRequestForApi(getMessagesT, options) ), diff --git a/ts/boot/__tests__/__snapshots__/persistedStore.test.ts.snap b/ts/boot/__tests__/__snapshots__/persistedStore.test.ts.snap index 57278305868..fff8594a02d 100644 --- a/ts/boot/__tests__/__snapshots__/persistedStore.test.ts.snap +++ b/ts/boot/__tests__/__snapshots__/persistedStore.test.ts.snap @@ -91,20 +91,6 @@ Object { "nameByFiscalCode": Object {}, }, "paymentByRptId": Object {}, - "services": Object { - "byId": Object {}, - "byOrgFiscalCode": Object {}, - "firstLoading": Object { - "isFirstServicesLoadingCompleted": false, - }, - "readState": Object {}, - "servicePreference": Object { - "kind": "PotNone", - }, - "visible": Object { - "kind": "PotNone", - }, - }, } `; diff --git a/ts/boot/configureStoreAndPersistor.ts b/ts/boot/configureStoreAndPersistor.ts index 588dd63430f..13acad3e92a 100644 --- a/ts/boot/configureStoreAndPersistor.ts +++ b/ts/boot/configureStoreAndPersistor.ts @@ -158,16 +158,7 @@ const migrations: MigrationManifest = { // Version 7 // we empty the services list to get both services list and services metadata being reloaded and persisted - "7": (state: PersistedState) => ({ - ...state, - entities: { - ...(state as PersistedGlobalState).entities, - services: { - ...(state as PersistedGlobalState).entities.services, - byId: {} - } - } - }), + "7": (state: PersistedState) => _.set(state, "entities.services.byId", {}), // Version 8 // we load services scope in an specific view. So now it is uselss to hold (old) services metadata diff --git a/ts/components/screens/BadgeComponent.tsx b/ts/components/screens/BadgeComponent.tsx index 315aabdffc9..fd4f3dd4b78 100644 --- a/ts/components/screens/BadgeComponent.tsx +++ b/ts/components/screens/BadgeComponent.tsx @@ -1,6 +1,6 @@ /** * A component to render the circolar badge - * TODO: use the same component on all lists (messages, services, transaction): https://www.pivotaltracker.com/story/show/167064275 + * TODO: use the same component on all lists (messages, transaction): https://www.pivotaltracker.com/story/show/167064275 */ import * as React from "react"; import { Circle, Svg } from "react-native-svg"; diff --git a/ts/components/screens/SectionHeaderComponent.tsx b/ts/components/screens/SectionHeaderComponent.tsx index 7c8870d39c8..672d8a10e2a 100644 --- a/ts/components/screens/SectionHeaderComponent.tsx +++ b/ts/components/screens/SectionHeaderComponent.tsx @@ -2,7 +2,6 @@ * A component to render a custom section header * TODO: use the same component for: * - message list https://www.pivotaltracker.com/story/show/165716236 - * - service lists https://www.pivotaltracker.com/story/show/166792020 */ import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; diff --git a/ts/components/search/SearchButton.tsx b/ts/components/search/SearchButton.tsx index dc5bb849189..90204e25411 100644 --- a/ts/components/search/SearchButton.tsx +++ b/ts/components/search/SearchButton.tsx @@ -9,7 +9,6 @@ import I18n from "../../i18n"; import { disableSearch, searchMessagesEnabled, - searchServicesEnabled, updateSearchText } from "../../store/actions/search"; import { Dispatch } from "../../store/actions/types"; @@ -18,7 +17,7 @@ import { ICON_BUTTON_MARGIN } from "../screens/BaseHeader"; export const MIN_CHARACTER_SEARCH_TEXT = 3; -export type SearchType = "Messages" | "Services"; +export type SearchType = "Messages"; interface OwnProps { color?: IOColors; @@ -129,9 +128,6 @@ const mapDispatchToProps = (dispatch: Dispatch, props: OwnProps) => ({ case "Messages": dispatch(searchMessagesEnabled(isSearchEnabled)); break; - case "Services": - dispatch(searchServicesEnabled(isSearchEnabled)); - break; } } }); diff --git a/ts/components/services/ContactPreferencesToggles/PreferenceToggleRow.tsx b/ts/components/services/ContactPreferencesToggles/PreferenceToggleRow.tsx deleted file mode 100644 index 87f3f8a8a19..00000000000 --- a/ts/components/services/ContactPreferencesToggles/PreferenceToggleRow.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from "react"; -import { View, StyleSheet } from "react-native"; -import { Icon, NativeSwitch } from "@pagopa/io-app-design-system"; -import { H4 } from "../../core/typography/H4"; -import { IOStyles } from "../../core/variables/IOStyles"; -import TouchableDefaultOpacity from "../../TouchableDefaultOpacity"; -import I18n from "../../../i18n"; -import { WithTestID } from "../../../types/WithTestID"; -import ActivityIndicator from "../../ui/ActivityIndicator"; - -type Props = WithTestID<{ - label: string; - onPress: (value: boolean) => void; - value: boolean; - graphicalState: "loading" | "error" | "ready"; - onReload: () => void; - disabled?: boolean; - accessiblityLabel?: string; -}>; - -const styles = StyleSheet.create({ - row: { - flexDirection: "row", - flex: 1, - justifyContent: "space-between", - paddingVertical: 12 - } -}); - -const PreferenceToggleRow = ({ - label, - onPress, - value, - graphicalState, - onReload, - disabled, - testID = "preference-toggle-row" -}: Props): React.ReactElement => { - const getComponentByGraphicalState = () => { - switch (graphicalState) { - case "loading": - return ( - - ); - case "error": - return ( - - - - ); - case "ready": - return ( - - ); - } - }; - return ( - - -

- {label} -

-
- {getComponentByGraphicalState()} -
- ); -}; - -export default PreferenceToggleRow; diff --git a/ts/components/services/ContactPreferencesToggles/__test__/ContactPreferencesToggles.test.tsx b/ts/components/services/ContactPreferencesToggles/__test__/ContactPreferencesToggles.test.tsx deleted file mode 100644 index ced4797c63e..00000000000 --- a/ts/components/services/ContactPreferencesToggles/__test__/ContactPreferencesToggles.test.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React from "react"; -import { Store } from "redux"; -import configureMockStore from "redux-mock-store"; -import { NotificationChannelEnum } from "../../../../../definitions/backend/NotificationChannel"; -import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import I18n from "../../../../i18n"; -import { applicationChangeState } from "../../../../store/actions/application"; -import { loadServicePreference } from "../../../../features/services/details/store/actions/preference"; -import { appReducer } from "../../../../store/reducers"; -import { GlobalState } from "../../../../store/reducers/types"; -import { ServicePreferenceResponse } from "../../../../features/services/details/types/ServicePreferenceResponse"; -import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; -import ContactPreferencesToggles from "../index"; - -jest.useFakeTimers(); - -describe("ContactPreferencesToggles component", () => { - it("should render the section header", () => { - const store = mockState({ - id: "some_id" as ServiceId, - kind: "success", - value: { - inbox: true, - email: true, - push: false, - can_access_message_read_status: false, - settings_version: 0 - } - }); - const component = renderComponent(store, {}); - expect( - component.getByText(I18n.t("serviceDetail.contacts.title")) - ).toBeDefined(); - }); - describe("when channels are not defined", () => { - it("should render all the switches", () => { - const store = mockState({ - id: "some_id" as ServiceId, - kind: "success", - value: { - inbox: true, - email: true, - push: false, - can_access_message_read_status: false, - settings_version: 0 - } - }); - const component = renderComponent(store, {}); - expect( - component.getByTestId("contact-preferences-inbox-switch") - ).toBeDefined(); - expect( - component.getByTestId("contact-preferences-webhook-switch") - ).toBeDefined(); - // TODO this option should be reintegrated once option will supported back from backend https://pagopa.atlassian.net/browse/IARS-17 - // expect( - // component.getByTestId("contact-preferences-email-switch") - // ).toBeDefined(); - }); - }); - - describe("when channels is an empty array", () => { - it("should render the INBOX switch", () => { - const store = mockState({ - id: "some_id" as ServiceId, - kind: "success", - value: { - inbox: true, - email: true, - push: false, - can_access_message_read_status: false, - settings_version: 0 - } - }); - const component = renderComponent(store, { channels: [] }); - expect( - component.getByTestId("contact-preferences-inbox-switch") - ).toBeDefined(); - }); - it("should not render WEBHOOK and EMAIL switches", () => { - const store = mockState({ - id: "some_id" as ServiceId, - kind: "success", - value: { - inbox: true, - email: true, - push: false, - can_access_message_read_status: false, - settings_version: 0 - } - }); - const component = renderComponent(store, { channels: [] }); - expect( - component.queryByTestId("contact-preferences-webhook-switch") - ).toBeNull(); - expect( - component.queryByTestId("contact-preferences-email-switch") - ).toBeNull(); - }); - }); - - describe("when channels contains all the items ", () => { - it("should render all the switches", () => { - const store = mockState({ - id: "some_id" as ServiceId, - kind: "success", - value: { - inbox: true, - email: true, - push: false, - can_access_message_read_status: false, - settings_version: 0 - } - }); - const component = renderComponent(store, { - channels: [ - NotificationChannelEnum.EMAIL, - NotificationChannelEnum.WEBHOOK - ] - }); - expect( - component.getByTestId("contact-preferences-inbox-switch") - ).toBeDefined(); - expect( - component.getByTestId("contact-preferences-webhook-switch") - ).toBeDefined(); - // TODO this option should be reintegrated once option will supported back from backend https://pagopa.atlassian.net/browse/IARS-17 - // expect( - // component.getByTestId("contact-preferences-email-switch") - // ).toBeDefined(); - }); - }); - - describe("when channels are loading", () => { - it("should render activity indicator on inbox", () => { - const initialState = appReducer( - undefined, - loadServicePreference.success({ - id: "some_id" as ServiceId, - kind: "success", - value: { - inbox: true, - email: true, - push: true, - can_access_message_read_status: false, - settings_version: 0 - } - }) - ); - // the store will be in someLoading - const state = appReducer( - initialState, - loadServicePreference.request("aServiceID" as ServiceId) - ); - const mockStore = configureMockStore(); - const store = mockStore({ - ...state - } as GlobalState); - const component = renderComponent(store, { - channels: [ - NotificationChannelEnum.EMAIL, - NotificationChannelEnum.WEBHOOK - ] - }); - expect( - component.getByTestId("contact-preferences-inbox-switch-loading") - ).toBeDefined(); - expect( - component.getByTestId("contact-preferences-webhook-switch-loading") - ).toBeDefined(); - }); - - it("should render activity indicator on inbox and webhook", () => { - const initialState = appReducer( - undefined, - applicationChangeState("active") - ); - const state = appReducer( - initialState, - loadServicePreference.request("aServiceID" as ServiceId) - ); - const mockStore = configureMockStore(); - const store = mockStore({ - ...state - } as GlobalState); - const component = renderComponent(store, { - channels: [ - NotificationChannelEnum.EMAIL, - NotificationChannelEnum.WEBHOOK - ] - }); - expect( - component.getByTestId("contact-preferences-inbox-switch-loading") - ).toBeDefined(); - }); - }); -}); - -const mockState = (servicePreference: ServicePreferenceResponse) => { - const initialState = appReducer(undefined, applicationChangeState("active")); - const state = appReducer( - initialState, - loadServicePreference.success(servicePreference) - ); - const mockStore = configureMockStore(); - return mockStore({ - ...state - } as GlobalState); -}; - -const renderComponent = ( - store: Store, - options: { - channels?: ReadonlyArray; - } -) => - renderScreenWithNavigationStoreContext( - () => ( - - ), - "route", - {}, - store - ); diff --git a/ts/components/services/ContactPreferencesToggles/__test__/PreferenceToggleRow.test.tsx b/ts/components/services/ContactPreferencesToggles/__test__/PreferenceToggleRow.test.tsx deleted file mode 100644 index a00c4f47abb..00000000000 --- a/ts/components/services/ContactPreferencesToggles/__test__/PreferenceToggleRow.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from "react"; -import { render, fireEvent } from "@testing-library/react-native"; - -import PreferenceToggleRow from "../PreferenceToggleRow"; - -describe("PreferenceToggleRow component", () => { - const options: Partial[0]> = { - label: "Push Notifications", - value: true, - onPress: jest.fn() - }; - it("should match the snapshot", () => { - const component = renderComponent(options); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it("should show the label", () => { - const component = renderComponent(options); - expect(component.getByText("Push Notifications")).toBeDefined(); - }); - it("should expose a working switch", () => { - const component = renderComponent(options); - const switchComponent = component.getByRole("switch"); - expect(switchComponent).toBeDefined(); - fireEvent(switchComponent, "onValueChange", false); - expect(options.onPress).toHaveBeenCalledWith(false); - }); - it("should use a default testID", () => { - const component = renderComponent(options); - expect(component.getByTestId("preference-toggle-row")).toBeDefined(); - }); - describe("when a testID is passed", () => { - it("should honour the property", () => { - const component = renderComponent({ ...options, testID: "new-test-id" }); - expect(component.getByTestId("new-test-id")).toBeDefined(); - }); - }); - - describe("handle different status", () => { - it("should display activity indicator", () => { - const component = renderComponent({ - ...options, - graphicalState: "loading" - }); - - expect( - component.getByTestId("preference-toggle-row-loading") - ).toBeDefined(); - }); - - it("should display reload button", () => { - const spy = jest.fn(); - const component = renderComponent({ - ...options, - graphicalState: "error", - onReload: spy - }); - const reloadComponent = component.getByTestId( - "preference-toggle-row-reload" - ); - expect(reloadComponent).toBeDefined(); - fireEvent.press(reloadComponent); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it("should display switch disabled", () => { - const component = renderComponent({ - ...options, - graphicalState: "ready", - disabled: true - }); - expect(component.getByTestId("preference-toggle-row")).toBeDefined(); - const switchComponent = component.getByRole("switch"); - expect(switchComponent).toBeDefined(); - expect(switchComponent).toHaveProp("disabled", true); - }); - }); -}); - -function renderComponent( - options: Partial[0]> -) { - const onPress = jest.fn(); - const onReload = jest.fn(); - return render( - - ); -} diff --git a/ts/components/services/ContactPreferencesToggles/__test__/__snapshots__/PreferenceToggleRow.test.tsx.snap b/ts/components/services/ContactPreferencesToggles/__test__/__snapshots__/PreferenceToggleRow.test.tsx.snap deleted file mode 100644 index 22b9ea6c100..00000000000 --- a/ts/components/services/ContactPreferencesToggles/__test__/__snapshots__/PreferenceToggleRow.test.tsx.snap +++ /dev/null @@ -1,83 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PreferenceToggleRow component should match the snapshot 1`] = ` - - - - Push Notifications - - - - -`; diff --git a/ts/components/services/ContactPreferencesToggles/index.tsx b/ts/components/services/ContactPreferencesToggles/index.tsx deleted file mode 100644 index c6e54eedeaa..00000000000 --- a/ts/components/services/ContactPreferencesToggles/index.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { IOToast } from "@pagopa/io-app-design-system"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useIsFocused } from "@react-navigation/native"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { connect } from "react-redux"; -import I18n from "i18n-js"; -import { NotificationChannelEnum } from "../../../../definitions/backend/NotificationChannel"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { trackPNPushSettings } from "../../../features/pn/analytics"; -import { - loadServicePreference, - upsertServicePreference -} from "../../../features/services/details/store/actions/preference"; -import { Dispatch } from "../../../store/actions/types"; -import { useIOSelector } from "../../../store/hooks"; -import { isPremiumMessagesOptInOutEnabledSelector } from "../../../store/reducers/backendStatus"; -import { - servicePreferenceSelector, - ServicePreferenceState -} from "../../../features/services/details/store/reducers/servicePreference"; -import { GlobalState } from "../../../store/reducers/types"; -import { - isServicePreferenceResponseSuccess, - ServicePreference -} from "../../../features/services/details/types/ServicePreferenceResponse"; -import { isStrictSome } from "../../../utils/pot"; -import ItemSeparatorComponent from "../../ItemSeparatorComponent"; -import SectionHeader from "../SectionHeader"; -import PreferenceToggleRow from "./PreferenceToggleRow"; - -type Item = "email" | "push" | "inbox" | "can_access_message_read_status"; - -type Props = { - channels?: ReadonlyArray; - serviceId: ServiceId; - isSpecialService: boolean; - customSpecialFlowOpt?: string; -} & ReturnType & - ReturnType; - -const hasChannel = ( - channel: NotificationChannelEnum, - channels?: ReadonlyArray -) => - pipe( - channels, - O.fromNullable, - O.map(anc => anc.indexOf(channel) !== -1), - O.getOrElse(() => true) - ); - -/** - * Utility function to get the user preference value for a specific channel - * return false if preference state is pot.none or if an error occurred on API Response - * */ -const getChannelPreference = ( - potServicePreference: ServicePreferenceState, - key: Item -): boolean => { - if ( - pot.isSome(potServicePreference) && - isServicePreferenceResponseSuccess(potServicePreference.value) - ) { - return potServicePreference.value.value[key]; - } - return false; -}; - -const ContactPreferencesToggle: React.FC = (props: Props) => { - const { isLoading, isError } = props; - const [isFirstRender, setIsFirstRender] = useState(true); - const { serviceId, loadServicePreference } = props; - - const loadPreferences = useCallback( - () => loadServicePreference(serviceId), - [serviceId, loadServicePreference] - ); - - const isFocused = useIsFocused(); - - const isPremiumMessagesOptInOutEnabled = useIOSelector( - isPremiumMessagesOptInOutEnabledSelector - ); - - useEffect(() => { - loadPreferences(); - }, [serviceId, loadPreferences, isFocused]); - - useEffect(() => { - if (!isFirstRender) { - if (isError) { - IOToast.error(I18n.t("global.genericError")); - } - } else { - setIsFirstRender(false); - } - }, [isError, isFirstRender]); - - const onValueChange = (value: boolean, type: Item) => { - if ( - isStrictSome(props.servicePreferenceStatus) && - isServicePreferenceResponseSuccess(props.servicePreferenceStatus.value) - ) { - props.upsertServicePreference(props.serviceId, { - ...props.servicePreferenceStatus.value.value, - [type]: value - }); - } - }; - - const graphicalState = useMemo( - () => (isLoading ? "loading" : isError ? "error" : "ready"), - [isLoading, isError] - ); - - return ( - <> - - {/* - This Toggle is disabled if the current service is a Special Service cause user can - enable or disable the service only using the proper Special Service flow and not only tapping the specific toggle - */} - onValueChange(value, "inbox")} - disabled={props.isSpecialService} - graphicalState={graphicalState} - onReload={loadPreferences} - value={getChannelPreference(props.servicePreferenceStatus, "inbox")} - testID={"contact-preferences-inbox-switch"} - /> - - {hasChannel(NotificationChannelEnum.WEBHOOK, props.channels) && - getChannelPreference(props.servicePreferenceStatus, "inbox") && ( - // toggle is disabled if the inbox value is false to prevent inconsistent data - <> - { - pipe( - props.customSpecialFlowOpt, - O.fromNullable, - O.filter(customSpecialFlow => customSpecialFlow === "pn"), - O.fold( - () => undefined, - _ => trackPNPushSettings(value) - ) - ); - onValueChange(value, "push"); - }} - value={getChannelPreference( - props.servicePreferenceStatus, - "push" - )} - graphicalState={graphicalState} - onReload={loadPreferences} - testID={"contact-preferences-webhook-switch"} - /> - - - )} - {isPremiumMessagesOptInOutEnabled && - getChannelPreference(props.servicePreferenceStatus, "inbox") && ( - // toggle is disabled if the inbox value is false to prevent inconsistent data - <> - - onValueChange(value, "can_access_message_read_status") - } - value={getChannelPreference( - props.servicePreferenceStatus, - "can_access_message_read_status" - )} - graphicalState={graphicalState} - onReload={loadPreferences} - testID={"contact-preferences-trackSeen-switch"} - /> - - - )} - - {/* Email toggle is temporary removed until the feature will be enabled back from the backend */} - {/* TODO this option should be reintegrated once option will supported back from backend https://pagopa.atlassian.net/browse/IARS-17 */} - {/* {hasChannel(NotificationChannelEnum.EMAIL, props.channels) && getChannelPreference(props.servicePreferenceStatus, "inbox") && ( */} - {/* <> */} - {/* onValueChange(value, "email")} */} - {/* value={getChannelPreference(props.servicePreferenceStatus, "email")} */} - {/* graphicalState={graphicalState} */} - {/* isError={isError} */} - {/* testID={"contact-preferences-email-switch"} */} - {/* /> */} - {/* */} - {/* */} - {/* )} */} - - ); -}; - -const mapStateToProps = (state: GlobalState) => { - const servicePreferenceStatus = servicePreferenceSelector(state); - const isLoading = - pot.isLoading(servicePreferenceStatus) || - pot.isUpdating(servicePreferenceStatus); - - const isError = - pot.isError(servicePreferenceStatus) || - (isStrictSome(servicePreferenceStatus) && - servicePreferenceStatus.value.kind !== "success"); - - return { - isLoading, - isError, - servicePreferenceStatus - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - upsertServicePreference: (id: ServiceId, sp: ServicePreference) => - dispatch(upsertServicePreference.request({ id, ...sp })), - loadServicePreference: (id: ServiceId) => - dispatch(loadServicePreference.request(id)) -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(ContactPreferencesToggle); diff --git a/ts/components/services/LinkRow.tsx b/ts/components/services/LinkRow.tsx deleted file mode 100644 index b25001d0e87..00000000000 --- a/ts/components/services/LinkRow.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from "react"; -import { StyleSheet } from "react-native"; - -import { IOToast } from "@pagopa/io-app-design-system"; -import { TranslationKeys } from "../../../locales/locales"; -import I18n from "../../i18n"; -import { openWebUrl } from "../../utils/url"; - -import { Link } from "../core/typography/Link"; -import ItemSeparatorComponent from "../ItemSeparatorComponent"; - -const styles = StyleSheet.create({ - link: { - paddingVertical: 16 - } -}); - -type Props = { - text: TranslationKeys; - href: string; -}; - -const LinkRow = ({ text, href }: Props) => ( - <> - - openWebUrl(href, () => IOToast.error(I18n.t("global.jserror.title"))) - } - numberOfLines={1} - style={styles.link} - > - {I18n.t(text)} - - - -); - -export default LinkRow; diff --git a/ts/components/services/LocalServicesWebView.tsx b/ts/components/services/LocalServicesWebView.tsx deleted file mode 100644 index 82a889e0363..00000000000 --- a/ts/components/services/LocalServicesWebView.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { IOColors, IOToast, hexToRgba } from "@pagopa/io-app-design-system"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as E from "fp-ts/lib/Either"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import React from "react"; -import { StyleSheet, View } from "react-native"; -import WebView, { WebViewMessageEvent } from "react-native-webview"; -import { connect } from "react-redux"; -import { Dispatch } from "redux"; -import { ServiceId } from "../../../definitions/backend/ServiceId"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import { localServicesWebUrl } from "../../config"; -import { useTabItemPressWhenScreenActive } from "../../hooks/useTabItemPressWhenScreenActive"; -import I18n from "../../i18n"; -import { loadServiceDetail } from "../../features/services/details/store/actions/details"; -import { servicesByIdSelector } from "../../features/services/details/store/reducers/servicesById"; -import { GlobalState } from "../../store/reducers/types"; -import { isStrictSome } from "../../utils/pot"; -import { AVOID_ZOOM_JS, closeInjectedScript } from "../../utils/webview"; -import { withLightModalContext } from "../helpers/withLightModalContext"; -import GenericErrorComponent from "../screens/GenericErrorComponent"; -import { RefreshIndicator } from "../ui/RefreshIndicator"; - -type Props = { - onServiceSelect: (service: ServicePublic) => void; -} & ReturnType & - ReturnType; - -const opaqueBgColor = hexToRgba(IOColors.white, 0.5); - -const styles = StyleSheet.create({ - refreshIndicatorContainer: { - position: "absolute", - left: 0, - right: 0, - top: 0, - bottom: 0, - backgroundColor: opaqueBgColor, - justifyContent: "center", - alignItems: "center", - zIndex: 1000 - }, - genericError: { - flex: 1, - position: "absolute", - left: 0, - right: 0, - top: 0, - bottom: 0 - }, - webView: { - flex: 1 - } -}); -const renderLoading = () => ( - - - -); - -/** - * This component is basically a webview that loads an url showing local services - * It intercepts the request of loading a service and it does: - * - block that request from loading - * - extract from the request url the service id - * - load the selected service starting from the service id (load and error are handled) - */ -const LocalServicesWebView = (props: Props) => { - const [serviceIdToLoad, setServiceIdToLoad] = React.useState< - string | undefined - >(undefined); - const [webViewError, setWebViewError] = React.useState(false); - const webViewRef = React.createRef(); - - const scrollWebview = (x: number, y: number) => { - const script = `window.scrollTo(${x}, ${y})`; - webViewRef.current?.injectJavaScript(script); - }; - - useTabItemPressWhenScreenActive(() => scrollWebview(0, 0), true); - - const { servicesById, onServiceSelect } = props; - - React.useEffect(() => { - pipe( - serviceIdToLoad, - O.fromNullable, - O.chainNullableK(sid => servicesById[sid]), - O.map(servicePot => { - // if service has been loaded - if (isStrictSome(servicePot)) { - onServiceSelect(servicePot.value); - setServiceIdToLoad(undefined); - return; - } - if (pot.isError(servicePot)) { - IOToast.error(I18n.t("global.genericError")); - } - }) - ); - }, [servicesById, onServiceSelect, serviceIdToLoad]); - - const reloadWebView = () => { - if (webViewRef.current) { - webViewRef.current.reload(); - setWebViewError(false); - } - }; - - /** - * 'listen' on web message - * if a serviceId is sent: dispatch service loading request - * @param event - */ - const handleWebviewMessage = (event: WebViewMessageEvent) => { - pipe( - event.nativeEvent.data, - ServiceId.decode, - E.map(sId => { - setServiceIdToLoad(sId); - // request loading service - props.loadService(sId); - }) - ); - }; - - const isLoadingServiceLoading = pipe( - serviceIdToLoad, - O.fromNullable, - O.chainNullableK(sid => props.servicesById[sid]), - O.fold(() => false, pot.isLoading) - ); - return ( - <> - {isLoadingServiceLoading && renderLoading()} - - setWebViewError(true)} - onMessage={handleWebviewMessage} - startInLoadingState={true} - renderLoading={renderLoading} - javaScriptEnabled={true} - /> - {webViewError && ( - - - - )} - - ); -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - loadService: (serviceId: string) => - dispatch(loadServiceDetail.request(serviceId)) -}); - -const mapStateToProps = (state: GlobalState) => ({ - servicesById: servicesByIdSelector(state) -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withLightModalContext(LocalServicesWebView)); diff --git a/ts/components/services/NewServiceListItem.tsx b/ts/components/services/NewServiceListItem.tsx deleted file mode 100644 index 29c6469db17..00000000000 --- a/ts/components/services/NewServiceListItem.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as React from "react"; -import { GestureResponderEvent } from "react-native"; -import { ListItemNav } from "@pagopa/io-app-design-system"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import I18n from "../../i18n"; - -type Props = { - item: pot.Pot; - onSelect: (service: ServicePublic) => void; - hideSeparator: boolean; -}; - -const NewServiceListItem = (props: Props): React.ReactElement => { - const potService = props.item; - const onPress = pot.toUndefined( - pot.map(potService, service => () => props.onSelect(service)) - ); - - const serviceName = pot.fold( - potService, - () => I18n.t("global.remoteStates.loading"), - () => I18n.t("global.remoteStates.loading"), - () => I18n.t("global.remoteStates.notAvailable"), - () => I18n.t("global.remoteStates.notAvailable"), - service => service.service_name, - () => I18n.t("global.remoteStates.loading"), - service => service.service_name, - () => I18n.t("global.remoteStates.notAvailable") - ); - - return ( - void} - testID={serviceName} - /> - ); -}; - -export default NewServiceListItem; diff --git a/ts/components/services/SectionHeader.tsx b/ts/components/services/SectionHeader.tsx deleted file mode 100644 index 1d701068645..00000000000 --- a/ts/components/services/SectionHeader.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import { StyleSheet, View } from "react-native"; - -import { IOIcons, Icon, HSpacer } from "@pagopa/io-app-design-system"; -import { TranslationKeys } from "../../../locales/locales"; -import I18n from "../../i18n"; - -import { H3 } from "../core/typography/H3"; - -const styles = StyleSheet.create({ - header: { - flexDirection: "row", - paddingVertical: 8, - alignItems: "center" - } -}); - -type Props = { - iconName: IOIcons; - title: TranslationKeys; -}; - -/** - * Renders a header for any section in the service's details page - */ -const sectionHeader: React.FC = ({ iconName, title }) => ( - - - -

- {I18n.t(title)} -

-
-); - -export default sectionHeader; diff --git a/ts/components/services/ServiceList.tsx b/ts/components/services/ServiceList.tsx deleted file mode 100644 index a03dbb14199..00000000000 --- a/ts/components/services/ServiceList.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/** - * A component to render a list of services grouped by organization. - */ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import React from "react"; -import { - Animated, - ListRenderItemInfo, - NativeScrollEvent, - NativeSyntheticEvent, - RefreshControl, - SectionList, - SectionListData -} from "react-native"; -import { IOVisualCostants } from "@pagopa/io-app-design-system"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import { getLogoForOrganization } from "../../utils/organizations"; -import { - TabBarItemPressType, - withUseTabItemPressWhenScreenActive -} from "../helpers/withUseTabItemPressWhenScreenActive"; - -import ItemSeparatorComponent from "../ItemSeparatorComponent"; -import SectionHeaderComponent from "../screens/SectionHeaderComponent"; -import NewServiceListItem from "./NewServiceListItem"; - -type AnimatedProps = { - animated?: { - onScroll: (_: NativeSyntheticEvent) => void; - scrollEventThrottle?: number; - }; -}; - -type OwnProps = { - sections: ReadonlyArray>>; - isRefreshing: boolean; - onRefresh: () => void; - onSelect: (service: ServicePublic) => void; - ListEmptyComponent?: React.ComponentProps< - typeof SectionList - >["ListEmptyComponent"]; -}; - -type Props = OwnProps & AnimatedProps & TabBarItemPressType; - -class ServiceList extends React.Component { - componentDidMount() { - const { setHasInternalTab: setHasInternalTab, setTabPressCallback } = - this.props; - - setHasInternalTab(true); - setTabPressCallback(() => () => { - sectionListRef.current?.scrollToLocation({ - animated: true, - itemIndex: 0, - sectionIndex: 0, - viewOffset: 0 - }); - }); - } - - private renderServiceItem = ( - itemInfo: ListRenderItemInfo> - ) => ( - - ); - - private getServiceKey = ( - potService: pot.Pot, - index: number - ): string => - pot.getOrElse( - pot.map( - potService, - service => `${service.service_id}-${service.version}` - ), - `service-pot-${index}` - ); - - private renderServiceSectionHeader = (info: { - section: SectionListData>; - }): React.ReactNode => ( - - ); - - public render() { - const { sections, isRefreshing, onRefresh, ListEmptyComponent } = - this.props; - - const refreshControl = ( - - ); - - return ( - - ); - } -} - -const sectionListRef = React.createRef(); - -export default withUseTabItemPressWhenScreenActive(ServiceList); diff --git a/ts/components/services/ServiceMetadata/InformationRow.tsx b/ts/components/services/ServiceMetadata/InformationRow.tsx deleted file mode 100644 index bc89c79b197..00000000000 --- a/ts/components/services/ServiceMetadata/InformationRow.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from "react"; -import { View, StyleSheet } from "react-native"; -import { HSpacer } from "@pagopa/io-app-design-system"; -import { TranslationKeys } from "../../../../locales/locales"; -import { capitalize } from "../../../utils/strings"; -import I18n from "../../../i18n"; -import { H4 } from "../../core/typography/H4"; -import TouchableDefaultOpacity from "../../TouchableDefaultOpacity"; -import ItemSeparatorComponent from "../../ItemSeparatorComponent"; - -const styles = StyleSheet.create({ - touchable: { - flexDirection: "row", - marginVertical: 16 - }, - value: { - flexGrow: 1, - flexShrink: 1, - textAlign: "right" - } -}); - -type Props = { - value: string; - label: TranslationKeys; - onPress: () => void; - isLast?: boolean; - accessibilityLabel?: string; -}; - -const InformationRow = ({ - value, - label, - onPress, - isLast, - accessibilityLabel -}: Props) => ( - - -

- {capitalize(I18n.t(label))} -

- -

- {value} -

-
- {!isLast && } -
-); - -export default InformationRow; diff --git a/ts/components/services/ServiceMetadata/__tests__/InformationRow.test.tsx b/ts/components/services/ServiceMetadata/__tests__/InformationRow.test.tsx deleted file mode 100644 index a06f9af6a94..00000000000 --- a/ts/components/services/ServiceMetadata/__tests__/InformationRow.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import { render } from "@testing-library/react-native"; -import { constNull } from "fp-ts/lib/function"; - -import InformationRow from "../InformationRow"; - -describe("the InformationRow component", () => { - it("should match the snapshot", () => { - expect( - render( - - ).toJSON() - ).toMatchSnapshot(); - }); -}); diff --git a/ts/components/services/ServiceMetadata/__tests__/ServiceMetadata.test.tsx b/ts/components/services/ServiceMetadata/__tests__/ServiceMetadata.test.tsx deleted file mode 100644 index 40e4ab86560..00000000000 --- a/ts/components/services/ServiceMetadata/__tests__/ServiceMetadata.test.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; -import { fireEvent, render } from "@testing-library/react-native"; -import React from "react"; -import { testableGenServiceMetadataAccessibilityLabel } from "../"; -import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import { ServiceMetadata } from "../../../../../definitions/backend/ServiceMetadata"; -import { ServiceScopeEnum } from "../../../../../definitions/backend/ServiceScope"; -import { StandardServiceCategoryEnum } from "../../../../../definitions/backend/StandardServiceCategory"; -import { TranslationKeys } from "../../../../../locales/locales"; -import I18n from "../../../../i18n"; -import { capitalize } from "../../../../utils/strings"; -import * as utilsUrl from "../../../../utils/url"; -import ServiceMetadataComponent from "../../ServiceMetadata"; - -jest.mock("../../../../utils/platform"); - -const spyOpenWebUrl = jest.spyOn(utilsUrl, "openWebUrl"); - -const defaultServiceMetadata: ServiceMetadata = { - scope: ServiceScopeEnum.NATIONAL, - category: StandardServiceCategoryEnum.STANDARD -}; - -const defaultProps = { - getItemOnPress: jest.fn(), - isDebugModeEnabled: false, - organizationFiscalCode: "01234567891" as OrganizationFiscalCode, - serviceId: "ABC123" as ServiceId, - servicesMetadata: defaultServiceMetadata -}; - -const genServiceMetadataAccessibilityLabel = - testableGenServiceMetadataAccessibilityLabel!; - -describe("ServiceMetadata component", () => { - beforeEach(() => { - defaultProps.getItemOnPress.mockReset(); - spyOpenWebUrl.mockReset(); - }); - afterEach(() => { - jest.dontMock("../../../../utils/url"); - }); - - it("should render the section header", () => { - expect( - renderComponent({ ...defaultProps }).getByText( - I18n.t("services.contactsAndInfo") - ) - ).toBeDefined(); - }); - - describe("when debug mode is enabled", () => { - const currentOptions = { - ...defaultProps, - isDebugModeEnabled: true - }; - it("should render the serviceId label", () => { - expect( - renderComponent(currentOptions).getByText( - capitalize(I18n.t("global.id")) - ) - ).toBeDefined(); - }); - - it("should render the serviceId value", () => { - expect( - renderComponent(currentOptions).getByText(currentOptions.serviceId) - ).toBeDefined(); - }); - - it(`should call "getItemOnPress" with (${currentOptions.serviceId}, "COPY")`, () => { - renderComponent(currentOptions); - expect(currentOptions.getItemOnPress).toHaveBeenCalledWith( - currentOptions.serviceId, - "COPY" - ); - }); - }); - - describe("given an organizationFiscalCode", () => { - it("should render the organizationFiscalCode label", () => { - expect( - renderComponent(defaultProps).getByText( - capitalize(I18n.t("serviceDetail.fiscalCode")) - ) - ).toBeDefined(); - }); - - it("should render the organizationFiscalCode value", () => { - expect( - renderComponent(defaultProps).getByText( - defaultProps.organizationFiscalCode - ) - ).toBeDefined(); - }); - - it("should render the correct accessibility label", () => { - const a11yLabel = genServiceMetadataAccessibilityLabel( - I18n.t("serviceDetail.fiscalCode"), - defaultProps.organizationFiscalCode, - I18n.t("serviceDetail.fiscalCodeAccessibilityCopy") - ); - - expect( - renderComponent(defaultProps).getByA11yLabel(a11yLabel) - ).toBeDefined(); - }); - - it(`should call "getItemOnPress" with (${defaultProps.organizationFiscalCode}, "COPY")`, () => { - renderComponent(defaultProps); - expect(defaultProps.getItemOnPress).toHaveBeenCalledWith( - defaultProps.organizationFiscalCode, - "COPY" - ); - }); - }); - - [ - [ - "address", - "via genova", - "services.contactAddress", - "MAP", - "openMaps.openAddressOnMap" - ] - ].forEach(([name, value, label, action, hint]) => { - describe(`when ${name} is defined`, () => { - const currentOptions = { - ...defaultProps, - servicesMetadata: { - ...defaultServiceMetadata, - [name]: value - } - }; - it(`should render its label "${label}"`, () => { - expect( - renderComponent(currentOptions).getByText( - capitalize(I18n.t(label as TranslationKeys)) - ) - ).toBeDefined(); - }); - it(`should render its value "${value}"`, () => { - expect(renderComponent(currentOptions).getByText(value)).toBeDefined(); - }); - it(`should call "getItemOnPress" with ("${value}", ${action})`, () => { - renderComponent(currentOptions); - expect(currentOptions.getItemOnPress).toHaveBeenCalledWith( - value, - action - ); - }); - it("should render the correct accessibility label", () => { - const a11yLabel = genServiceMetadataAccessibilityLabel( - I18n.t(label as TranslationKeys), - value, - I18n.t(hint as TranslationKeys) - ); - - expect( - renderComponent(currentOptions).getByA11yLabel(a11yLabel) - ).toBeDefined(); - }); - }); - }); - - [ - ["email", "jest@test.com", "global.media.email", "mailto:"], - ["pec", "jest.pec@test.com", "global.media.pec", "mailto:"], - ["phone", "12341234", "global.media.phone", "tel:"] - ].forEach(([name, value, label, prefix]) => { - describe(`when ${name} is defined`, () => { - const currentOptions = { - ...defaultProps, - servicesMetadata: { ...defaultServiceMetadata, [name]: value } - }; - // eslint-disable-next-line sonarjs/no-identical-functions - it(`should render its label "${label}"`, () => { - expect( - renderComponent(currentOptions).getByText( - capitalize(I18n.t(label as TranslationKeys)) - ) - ).toBeDefined(); - }); - it(`should render its value "${value}"`, () => { - expect(renderComponent(currentOptions).getByText(value)).toBeDefined(); - }); - it(`should call "getItemOnPress" with ("${prefix}:${value}")`, () => { - renderComponent(currentOptions); - expect(currentOptions.getItemOnPress).toHaveBeenCalledWith( - `${prefix}${value}` - ); - }); - }); - }); - - [ - ["support_url", "www.support.it", "services.askForAssistance"], - ["web_url", "www.product.it", "services.visitWebsite"] - ].forEach(([name, value, label]) => { - const currentOptions = { - ...defaultProps, - servicesMetadata: { - ...defaultServiceMetadata, - [name]: value - } - }; - describe(`when ${name} is defined`, () => { - it(`should render a link with "${label}"`, () => { - const component = renderComponent(currentOptions); - const link = component.getByRole("link"); - expect(link).toBeDefined(); - expect(link.children.toString()).toMatch( - I18n.t(label as TranslationKeys) - ); - }); - describe("when the link is pressed", () => { - it(`should open the url "${value}"`, () => { - const component = renderComponent(currentOptions); - const link = component.getByRole("link"); - fireEvent(link, "onPress"); - expect(spyOpenWebUrl).toHaveBeenCalledWith( - value, - expect.any(Function) - ); - }); - }); - }); - }); - - describe("when the platform is Android", () => { - beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - require("../../../../utils/platform").test_setPlatform("android"); - }); - - describe(`and servicesMetadata.app_android is defined`, () => { - const androidUrl = "http://www.android.google"; - const currentOptions = { - ...defaultProps, - servicesMetadata: { - ...defaultServiceMetadata, - app_android: androidUrl - } as ServiceMetadata - }; - it(`should render the Android link`, () => { - const component = renderComponent(currentOptions); - const link = component.getByRole("link"); - expect(link.children.toString()).toMatch( - I18n.t("services.otherAppAndroid") - ); - }); - it(`the link should open ${androidUrl}`, () => { - const component = renderComponent(currentOptions); - const link = component.getByRole("link"); - fireEvent(link, "onPress"); - expect(spyOpenWebUrl).toHaveBeenCalledWith( - androidUrl, - expect.any(Function) - ); - }); - }); - - describe(`and servicesMetadata.app_ios is defined`, () => { - const currentOptions = { - ...defaultProps, - servicesMetadata: { - ...defaultServiceMetadata, - app_ios: "dummy" - } as ServiceMetadata - }; - it(`should not render it`, () => { - expect(renderComponent(currentOptions).queryByRole("link")).toBeNull(); - }); - }); - }); - - describe("when the platform is iOS", () => { - beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - require("../../../../utils/platform").test_setPlatform("ios"); - }); - - describe(`and servicesMetadata.app_ios is defined`, () => { - const iosUrl = "http://www.ios.apple"; - const currentOptions = { - ...defaultProps, - servicesMetadata: { - ...defaultServiceMetadata, - app_ios: iosUrl - } as ServiceMetadata - }; - it(`should render the iOS link`, () => { - const component = renderComponent(currentOptions); - const link = component.getByRole("link"); - expect(link.children.toString()).toMatch( - I18n.t("services.otherAppIos") - ); - }); - it(`the link should open ${iosUrl}`, () => { - const component = renderComponent(currentOptions); - const link = component.getByRole("link"); - fireEvent(link, "onPress"); - expect(spyOpenWebUrl).toHaveBeenCalledWith( - iosUrl, - expect.any(Function) - ); - }); - }); - - describe(`and servicesMetadata.app_android is defined`, () => { - const currentOptions = { - ...defaultProps, - servicesMetadata: { - ...defaultServiceMetadata, - app_android: "dummy" - } as ServiceMetadata - }; - it(`should not render it`, () => { - expect(renderComponent(currentOptions).queryByRole("link")).toBeNull(); - }); - }); - }); -}); - -function renderComponent( - props: Parameters[0] -) { - return render(); -} diff --git a/ts/components/services/ServiceMetadata/__tests__/__snapshots__/InformationRow.test.tsx.snap b/ts/components/services/ServiceMetadata/__tests__/__snapshots__/InformationRow.test.tsx.snap deleted file mode 100644 index 907c0eb4256..00000000000 --- a/ts/components/services/ServiceMetadata/__tests__/__snapshots__/InformationRow.test.tsx.snap +++ /dev/null @@ -1,112 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`the InformationRow component should match the snapshot 1`] = ` - - - - Institution's Fiscal Code - - - - via Roma - - - - -`; diff --git a/ts/components/services/ServiceMetadata/index.tsx b/ts/components/services/ServiceMetadata/index.tsx deleted file mode 100644 index 89c9c00b2ca..00000000000 --- a/ts/components/services/ServiceMetadata/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; -import React from "react"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { ServiceMetadata } from "../../../../definitions/backend/ServiceMetadata"; -import I18n from "../../../i18n"; -import { isTestEnv } from "../../../utils/environment"; -import { isAndroid, isIos } from "../../../utils/platform"; -import { ItemAction } from "../../../utils/url"; -import LinkRow from ".././LinkRow"; -import SectionHeader from ".././SectionHeader"; -import InformationRow from "./InformationRow"; - -type Props = { - getItemOnPress: (value: string, valueType?: ItemAction) => () => void; - isDebugModeEnabled: boolean; - organizationFiscalCode: OrganizationFiscalCode; - serviceId: ServiceId; - servicesMetadata?: ServiceMetadata; -}; - -/** - * Function used to generate the `accessibilityLabel` for a single - * row in the `ServiceMetadataComponent`. Given the `field`, `value, - * and `hint` it creates a custom label that contains all these - * informations. - */ -const genServiceMetadataAccessibilityLabel = ( - field: string, - value: string, - hint: string -) => `${field}: ${value}, ${hint}`; - -export const testableGenServiceMetadataAccessibilityLabel = isTestEnv - ? genServiceMetadataAccessibilityLabel - : undefined; - -/** - * Renders a dedicated section with a service's metadata and the header. - */ -const ServiceMetadataComponent: React.FC = ({ - organizationFiscalCode, - getItemOnPress, - serviceId, - servicesMetadata, - isDebugModeEnabled -}: Props) => { - const { - address, - app_android, - email, - app_ios, - pec, - phone, - support_url, - web_url - } = servicesMetadata || {}; - return ( - <> - - - {/* links */} - {web_url && } - {support_url && ( - - )} - {isIos && app_ios && ( - - )} - {isAndroid && app_android && ( - - )} - - {/* touchable rows */} - { - - } - {address && ( - - )} - {phone && ( - - )} - {email && ( - - )} - {pec && ( - - )} - {isDebugModeEnabled && serviceId && ( - - )} - - ); -}; - -export default ServiceMetadataComponent; diff --git a/ts/components/services/ServicesSearch.tsx b/ts/components/services/ServicesSearch.tsx deleted file mode 100644 index acb7d6cb7b0..00000000000 --- a/ts/components/services/ServicesSearch.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/** - * A component that renders a list of services that match a search text. - * TODO: fix scroll: some items are displayed only if the keyboard is hidden - * https://www.pivotaltracker.com/story/show/168803731 - */ - -import * as pot from "@pagopa/ts-commons/lib/pot"; -import React from "react"; -import { SectionListData } from "react-native"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import { ServicesSectionState } from "../../store/reducers/entities/services"; -import { isDefined } from "../../utils/guards"; -import { serviceContainsText } from "../../utils/services"; -import { SearchNoResultMessage } from "../search/SearchNoResultMessage"; -import ServicesSectionsList from "./ServicesSectionsList"; - -type OwnProps = { - sectionsState: ReadonlyArray; - searchText: string; - onRefresh: () => void; - navigateToServiceDetail: (service: ServicePublic) => void; -}; - -type Props = OwnProps; - -type State = { - potFilteredServiceSectionsStates: pot.Pot< - ReadonlyArray, - Error - >; -}; - -/** - * Filter only the services that match the searchText. - */ -const generateSectionsServicesStateMatchingSearchTextArrayAsync = ( - servicesState: ReadonlyArray, - searchText: string -): Promise> => - new Promise(resolve => { - const result = servicesState - .map(section => - filterSectionListDataMatchingSearchText(section, searchText) - ) - .filter(isDefined); - - resolve(result); - }); - -function filterSectionListDataMatchingSearchText( - sectionListData: SectionListData>, - searchText: string -) { - const filteredData = sectionListData.data - .map(potService => - pot.filter(potService, servicePublic => - // Search in service properties - serviceContainsText(servicePublic, searchText) - ) - ) - .filter(pot.isSome); - - const sectionListDataFiltered = { - organizationName: sectionListData.organizationName, - organizationFiscalCode: sectionListData.organizationFiscalCode, - data: filteredData - }; - return filteredData.length > 0 ? sectionListDataFiltered : null; -} - -class ServicesSearch extends React.PureComponent { - constructor(props: Props) { - super(props); - this.state = { - potFilteredServiceSectionsStates: pot.noneLoading - }; - } - - public async componentDidMount() { - const { sectionsState, searchText } = this.props; - const { potFilteredServiceSectionsStates } = this.state; - - // Set filtering status - this.setState({ - potFilteredServiceSectionsStates: pot.toLoading( - potFilteredServiceSectionsStates - ) - }); - - // Start filtering services - const filteredServiceSectionsStates = - await generateSectionsServicesStateMatchingSearchTextArrayAsync( - sectionsState, - searchText - ); - - // Unset filtering status - this.setState({ - potFilteredServiceSectionsStates: pot.some(filteredServiceSectionsStates) - }); - } - - public async componentDidUpdate(prevProps: Props) { - const { sectionsState: prevServicesState, searchText: prevSearchText } = - prevProps; - const { sectionsState, searchText } = this.props; - const { potFilteredServiceSectionsStates } = this.state; - - if (sectionsState !== prevServicesState || searchText !== prevSearchText) { - // Set filtering status - this.setState({ - potFilteredServiceSectionsStates: pot.toLoading( - potFilteredServiceSectionsStates - ) - }); - - // Start filtering services - const filteredServiceSectionsStates = - await generateSectionsServicesStateMatchingSearchTextArrayAsync( - sectionsState, - searchText - ); - - // Unset filtering status - this.setState({ - potFilteredServiceSectionsStates: pot.some( - filteredServiceSectionsStates - ) - }); - } - } - - public render() { - const { potFilteredServiceSectionsStates } = this.state; - const { onRefresh } = this.props; - - const isFiltering = pot.isLoading(potFilteredServiceSectionsStates); - - const filteredServiceSectionsStates = pot.getOrElse( - potFilteredServiceSectionsStates, - [] - ); - - return filteredServiceSectionsStates.length > 0 ? ( - - ) : ( - - ); - } - - private handleOnServiceSelect = (service: ServicePublic) => { - this.props.navigateToServiceDetail(service); - }; -} - -export default ServicesSearch; diff --git a/ts/components/services/ServicesSectionsList.tsx b/ts/components/services/ServicesSectionsList.tsx deleted file mode 100644 index 9e30ae9ea87..00000000000 --- a/ts/components/services/ServicesSectionsList.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/** - * A component to render a list of services organized in sections, one for each organization. - */ -import { VSpacer } from "@pagopa/io-app-design-system"; -import React from "react"; -import { - Image, - NativeScrollEvent, - NativeSyntheticEvent, - StyleSheet, - View -} from "react-native"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import I18n from "../../i18n"; -import { ServicesSectionState } from "../../store/reducers/entities/services"; -import customVariables from "../../theme/variables"; -import { Body } from "../core/typography/Body"; -import { IOStyles } from "../core/variables/IOStyles"; -import ServiceList from "./ServiceList"; - -type AnimatedProps = { - animated?: { - onScroll: (_: NativeSyntheticEvent) => void; - scrollEventThrottle?: number; - }; -}; - -type OwnProps = { - sections: ReadonlyArray; - isRefreshing: boolean; - onRefresh: () => void; - onSelect: (service: ServicePublic) => void; -}; - -type Props = AnimatedProps & OwnProps; - -const styles = StyleSheet.create({ - contentWrapper: { - flex: 1 - }, - headerContentWrapper: { - paddingTop: customVariables.contentPadding / 2, - paddingBottom: customVariables.contentPadding / 2, - alignItems: "center" - } -}); - -// component used when the list is empty -const emptyListComponent = () => ( - - - - - - {I18n.t("services.emptyListMessage")} - - -); - -const ServicesSectionsList = (props: Props) => ( - - {/* TODO: This is a workaround to make sure that the list is not placed under the tab bar - https://pagopa.atlassian.net/jira/software/projects/IOAPPFD0/boards/313?selectedIssue=IOAPPFD0-40 */} - - - -); - -export default ServicesSectionsList; diff --git a/ts/components/services/ServicesTab.tsx b/ts/components/services/ServicesTab.tsx deleted file mode 100644 index b38b8556e7b..00000000000 --- a/ts/components/services/ServicesTab.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * A component to render a tab containing a list of services organized in sections - */ -import * as React from "react"; -import { Animated } from "react-native"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import { ServicesSectionState } from "../../store/reducers/entities/services"; -import { withLightModalContext } from "../helpers/withLightModalContext"; -import { LightModalContextInterface } from "../ui/LightModal"; -import ServicesSectionsList from "./ServicesSectionsList"; - -type OwnProps = Readonly<{ - sections: ReadonlyArray; - isRefreshing: boolean; - onRefresh: (hideToast?: boolean) => void; // eslint-disable-line - onServiceSelect: (service: ServicePublic) => void; - tabScrollOffset: Animated.Value; -}>; - -type Props = OwnProps & LightModalContextInterface; - -const ServicesTab = (props: Props): React.ReactElement => { - const onTabScroll = () => ({ - onScroll: Animated.event([ - { - nativeEvent: { - contentOffset: { y: props.tabScrollOffset } - } - } - ]), - scrollEventThrottle: 8 - }); - - return ( - - ); -}; - -export default withLightModalContext(ServicesTab); diff --git a/ts/components/services/SpecialServices/LegacySpecialServicesCTA.tsx b/ts/components/services/SpecialServices/LegacySpecialServicesCTA.tsx deleted file mode 100644 index 9cda98014f0..00000000000 --- a/ts/components/services/SpecialServices/LegacySpecialServicesCTA.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import * as React from "react"; -import { constNull, pipe } from "fp-ts/lib/function"; -import * as B from "fp-ts/lib/boolean"; -import * as O from "fp-ts/lib/Option"; -import { ButtonSolid } from "@pagopa/io-app-design-system"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { cdcEnabled } from "../../../config"; -import CdcServiceCTA from "../../../features/bonus/cdc/components/CdcServiceCTA"; -import LegacyCgnServiceCTA from "../../../features/bonus/cgn/components/LegacyCgnServiceCTA"; -import LegacyPnServiceCTA from "../../../features/pn/components/LegacyServiceCTA"; -import I18n from "../../../i18n"; -import { useIOSelector } from "../../../store/hooks"; -import { - isCdcEnabledSelector, - isCGNEnabledSelector, - isPnEnabledSelector, - isPnSupportedSelector -} from "../../../store/reducers/backendStatus"; -import { openAppStoreUrl } from "../../../utils/url"; - -type SpecialServiceConfig = { - isEnabled: boolean; - isSupported: boolean; -}; - -type Props = { - customSpecialFlowOpt?: string; - serviceId: ServiceId; - activate?: boolean; -}; - -const UpdateAppCTA = () => { - // utility to open the app store on the OS - const openAppStore = React.useCallback(() => openAppStoreUrl(), []); - - return ( - - ); -}; - -const renderCta = ( - isEnabled: boolean, - isSupported: boolean, - cta: JSX.Element -) => - pipe( - isEnabled, - B.fold(constNull, () => - pipe( - isSupported, - B.fold( - () => , - () => cta - ) - ) - ) - ); - -const LegacySpecialServicesCTA = (props: Props) => { - const { customSpecialFlowOpt } = props; - - const isCGNEnabled = useIOSelector(isCGNEnabledSelector); - const cdcEnabledSelector = useIOSelector(isCdcEnabledSelector); - - const isCdcEnabled = cdcEnabledSelector && cdcEnabled; - - const isPnEnabled = useIOSelector(isPnEnabledSelector); - const isPnSupported = useIOSelector(isPnSupportedSelector); - - const mapSpecialServiceConfig = new Map([ - ["cgn", { isEnabled: isCGNEnabled, isSupported: true }], - ["cdc", { isEnabled: isCdcEnabled, isSupported: true }], - ["pn", { isEnabled: isPnEnabled, isSupported: isPnSupported }] - ]); - - return pipe( - customSpecialFlowOpt, - O.fromNullable, - O.fold(constNull, csf => - pipe( - mapSpecialServiceConfig.get(csf), - O.fromNullable, - O.fold( - () => , - ({ isEnabled, isSupported }) => { - switch (csf) { - case "cgn": - return renderCta( - isEnabled, - isSupported, - - ); - case "cdc": - return renderCta(isEnabled, isSupported, ); - case "pn": - return renderCta( - isEnabled, - isSupported, - - ); - default: - return ; - } - } - ) - ) - ) - ); -}; - -export default LegacySpecialServicesCTA; diff --git a/ts/components/services/TosAndPrivacyBox.tsx b/ts/components/services/TosAndPrivacyBox.tsx deleted file mode 100644 index 3beb2e53496..00000000000 --- a/ts/components/services/TosAndPrivacyBox.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; -import { View } from "react-native"; - -import LinkRow from "./LinkRow"; -import SectionHeader from "./SectionHeader"; - -type Props = { - privacyUrl?: string; - tosUrl?: string; -}; - -/** - * Renders a dedicated section with TOS, Privacy urls, and the header. - * It **doesn't render** if both links are not defined! - */ -const TosAndPrivacy: React.FC = ({ tosUrl, privacyUrl }) => { - if (tosUrl === undefined && privacyUrl === undefined) { - return null; - } - return ( - - - {tosUrl && } - {privacyUrl && } - - ); -}; - -export default TosAndPrivacy; diff --git a/ts/components/services/__tests__/LinkRow.test.tsx b/ts/components/services/__tests__/LinkRow.test.tsx deleted file mode 100644 index dae0e1b439f..00000000000 --- a/ts/components/services/__tests__/LinkRow.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import { render } from "@testing-library/react-native"; - -import LinkRow from "../LinkRow"; - -describe("LinkRow component", () => { - it("should match the snapshot", () => { - const component = render( - - ); - expect(component.toJSON()).toMatchSnapshot(); - }); -}); diff --git a/ts/components/services/__tests__/SectionHeader.test.tsx b/ts/components/services/__tests__/SectionHeader.test.tsx deleted file mode 100644 index 6d62e9fb5b7..00000000000 --- a/ts/components/services/__tests__/SectionHeader.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import { render } from "@testing-library/react-native"; - -import SectionHeader from "../SectionHeader"; - -describe("SectionHeader component", () => { - it("should match the snapshot", () => { - const component = render( - - ); - expect(component.toJSON()).toMatchSnapshot(); - }); -}); diff --git a/ts/components/services/__tests__/TosAndPrivacyBox.test.tsx b/ts/components/services/__tests__/TosAndPrivacyBox.test.tsx deleted file mode 100644 index e37277a0504..00000000000 --- a/ts/components/services/__tests__/TosAndPrivacyBox.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { fireEvent, render } from "@testing-library/react-native"; -import React from "react"; - -import { IOToast } from "@pagopa/io-app-design-system"; -import I18n from "../../../i18n"; -import TosAndPrivacyBox from "../TosAndPrivacyBox"; - -// eslint-disable-next-line functional/no-let -let MOCK_URL_WILL_FAIL = false; - -const mockOpenWebUrl = jest.fn(); - -jest.mock("../../../utils/url", () => ({ - openWebUrl: (_: string, onError: () => void) => { - mockOpenWebUrl(); - // we rely on an internal of `openWebUrl`, this might be improved? - if (MOCK_URL_WILL_FAIL) { - onError(); - } - } -})); - -const options = { - tosUrl: "https://www.fsf.org/", - privacyUrl: "https://gnupg.org/" -}; - -describe("TosAndPrivacyBox component", () => { - beforeEach(() => { - mockOpenWebUrl.mockReset(); - MOCK_URL_WILL_FAIL = false; - }); - - it("should have one header", () => { - const component = renderComponent({ ...options }); - expect(component.getByRole("header")).toBeDefined(); - expect(component.getByText(I18n.t("services.tosAndPrivacy"))).toBeDefined(); - }); - - describe("when both URLs are defined", () => { - it("should call `openWebUrl` for TOS link", () => { - const component = renderComponent(options); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const link = component - .getAllByRole("link") - .find(item => item.children[0] === I18n.t("services.tosLink"))!; - fireEvent(link, "onPress"); - expect(mockOpenWebUrl).toHaveBeenCalledTimes(1); - }); - - it("should call `openWebUrl` for Privacy link", () => { - const component = renderComponent(options); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const link = component - .getAllByRole("link") - .find(item => item.children[0] === I18n.t("services.privacyLink"))!; - fireEvent(link, "onPress"); - expect(mockOpenWebUrl).toHaveBeenCalledTimes(1); - }); - - it("should call `showToast` when then link fails", () => { - MOCK_URL_WILL_FAIL = true; - const component = renderComponent(options); - const showToastSpy = jest.spyOn(IOToast, "error"); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const link = component - .getAllByRole("link") - .find(item => item.children[0] === I18n.t("services.privacyLink"))!; - fireEvent(link, "onPress"); - expect(showToastSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe("when either URL is not defined", () => { - it("should not render TOS link", () => { - expect( - renderComponent({ ...options, tosUrl: undefined }) - .getAllByRole("link") - .find(item => item.children[0] === I18n.t("services.tosLink")) - ).toBeUndefined(); - }); - - it("should not render Privacy link", () => { - expect( - renderComponent({ ...options, privacyUrl: undefined }) - .getAllByRole("link") - .find(item => item.children[0] === I18n.t("services.privacyLink")) - ).toBeUndefined(); - }); - }); - - describe("when neither URL is defined", () => { - it("should not render anything", () => { - expect(renderComponent({}).toJSON()).toEqual(null); - }); - }); -}); - -function renderComponent({ - tosUrl, - privacyUrl -}: Parameters[0]) { - return render(); -} diff --git a/ts/components/services/__tests__/__snapshots__/LinkRow.test.tsx.snap b/ts/components/services/__tests__/__snapshots__/LinkRow.test.tsx.snap deleted file mode 100644 index aae05bcac23..00000000000 --- a/ts/components/services/__tests__/__snapshots__/LinkRow.test.tsx.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LinkRow component should match the snapshot 1`] = ` -Array [ - - Email - , - , -] -`; diff --git a/ts/components/services/__tests__/__snapshots__/SectionHeader.test.tsx.snap b/ts/components/services/__tests__/__snapshots__/SectionHeader.test.tsx.snap deleted file mode 100644 index 792bd226546..00000000000 --- a/ts/components/services/__tests__/__snapshots__/SectionHeader.test.tsx.snap +++ /dev/null @@ -1,110 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SectionHeader component should match the snapshot 1`] = ` - - - - - - - - - ID - - -`; diff --git a/ts/config.ts b/ts/config.ts index ebdd49c6fe9..174c24247fa 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -36,9 +36,6 @@ const DEFAULT_FAST_LOGIN_MAX_RETRIES = 3; // Default number of workers to fetch message. const DEFAULT_TOT_MESSAGE_FETCH_WORKERS = 5; -// Default number of workers to fetch service. -const DEFAULT_TOT_SERVICE_FETCH_WORKERS = 5; - // TODO: calculate the page size based on available screen space and item's height // https://pagopa.atlassian.net/browse/IA-474 const DEFAULT_PAGE_SIZE = 12; @@ -151,12 +148,6 @@ export const totMessageFetchWorkers = pipe( E.getOrElse(() => DEFAULT_TOT_MESSAGE_FETCH_WORKERS) ); -export const totServiceFetchWorkers = pipe( - parseInt(Config.TOT_SERVICE_FETCH_WORKERS, 10), - t.Integer.decode, - E.getOrElse(() => DEFAULT_TOT_SERVICE_FETCH_WORKERS) -); - export const shufflePinPadOnPayment = Config.SHUFFLE_PINPAD_ON_PAYMENT === "YES"; @@ -172,12 +163,6 @@ export const zendeskPrivacyUrl: string = pipe( E.getOrElse(() => "https://www.pagopa.it/it/privacy-policy-assistenza/") ); -export const localServicesWebUrl: string = pipe( - Config.LOCAL_SERVICE_WEB_URL, - t.string.decode, - E.getOrElse(() => "https://io.italia.it") -); - export const unsupportedDeviceMoreInfoUrl: string = pipe( Config.UNSUPPORTED_DEVICE_MORE_INFO_URL, NonEmptyString.decode, diff --git a/ts/features/bonus/cgn/components/CgnServiceCTA.tsx b/ts/features/bonus/cgn/components/CgnServiceCTA.tsx index cb42ca4f7a9..9271c0cb0e1 100644 --- a/ts/features/bonus/cgn/components/CgnServiceCTA.tsx +++ b/ts/features/bonus/cgn/components/CgnServiceCTA.tsx @@ -6,9 +6,9 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import I18n from "../../../../i18n"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { - servicePreferenceResponseSuccessSelector, - servicePreferenceSelector -} from "../../../services/details/store/reducers/servicePreference"; + servicePreferencePotSelector, + servicePreferenceResponseSuccessSelector +} from "../../../services/details/store/reducers"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { cgnActivationStart } from "../store/actions/activation"; import { cgnUnsubscribe } from "../store/actions/unsubscribe"; @@ -31,7 +31,7 @@ export const CgnServiceCta = ({ serviceId }: CgnServiceCtaProps) => { servicePreferenceResponseSuccessSelector ); - const servicePreferencePot = useIOSelector(servicePreferenceSelector); + const servicePreferencePot = useIOSelector(servicePreferencePotSelector); const unsubscriptionStatus = useIOSelector(cgnUnsubscribeSelector); diff --git a/ts/features/bonus/cgn/components/LegacyCgnServiceCTA.tsx b/ts/features/bonus/cgn/components/LegacyCgnServiceCTA.tsx index 810dd4b461d..9beb95491ad 100644 --- a/ts/features/bonus/cgn/components/LegacyCgnServiceCTA.tsx +++ b/ts/features/bonus/cgn/components/LegacyCgnServiceCTA.tsx @@ -10,7 +10,7 @@ import { } from "@pagopa/io-app-design-system"; import I18n from "../../../../i18n"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { servicePreferenceSelector } from "../../../services/details/store/reducers/servicePreference"; +import { servicePreferencePotSelector } from "../../../services/details/store/reducers"; import { isServicePreferenceResponseSuccess } from "../../../services/details/types/ServicePreferenceResponse"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { cgnActivationStart } from "../store/actions/activation"; @@ -27,10 +27,10 @@ type Props = { const LegacyCgnServiceCTA = (props: Props) => { const isFirstRender = useRef(true); const dispatch = useIODispatch(); - const servicePreference = useIOSelector(servicePreferenceSelector); + const servicePreferencePot = useIOSelector(servicePreferencePotSelector); const unsubscriptionStatus = useIOSelector(cgnUnsubscribeSelector); - const servicePreferenceValue = pot.getOrElse(servicePreference, undefined); + const servicePreferenceValue = pot.getOrElse(servicePreferencePot, undefined); useEffect(() => { if (!isFirstRender.current) { diff --git a/ts/features/bonus/common/screens/AvailableBonusScreen.tsx b/ts/features/bonus/common/screens/AvailableBonusScreen.tsx index 62465364bee..f9b6665cc16 100644 --- a/ts/features/bonus/common/screens/AvailableBonusScreen.tsx +++ b/ts/features/bonus/common/screens/AvailableBonusScreen.tsx @@ -16,7 +16,6 @@ import { } from "react-native"; import { connect } from "react-redux"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import { ServicePublic } from "../../../../../definitions/backend/ServicePublic"; import { BonusAvailable } from "../../../../../definitions/content/BonusAvailable"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; import BaseScreenComponent, { @@ -28,7 +27,6 @@ import { navigateBack, navigateToServiceDetailsScreen } from "../../../../store/actions/navigation"; -import { showServiceDetails } from "../../../../store/actions/services"; import { Dispatch } from "../../../../store/actions/types"; import { isCGNEnabledSelector, @@ -113,12 +111,10 @@ class AvailableBonusScreen extends React.PureComponent { // TODO: add mixpanel tracking and alert: https://pagopa.atlassian.net/browse/AP-14 IOToast.show(I18n.t("bonus.cdc.serviceEntryPoint.notAvailable")); }, - s => () => { - this.props.showServiceDetails(s); + s => () => this.props.navigateToServiceDetailsScreen({ serviceId: s.service_id - }); - } + }) ) ); }); @@ -242,9 +238,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ navigateToServiceDetailsScreen(params), serviceDetailsLoad: (serviceId: ServiceId) => { dispatch(loadServiceDetail.request(serviceId)); - }, - showServiceDetails: (service: ServicePublic) => - dispatch(showServiceDetails(service)) + } }); const AvailableBonusScreenFC: React.FunctionComponent = ( diff --git a/ts/features/bonus/common/store/selectors/index.ts b/ts/features/bonus/common/store/selectors/index.ts index 9f02c7c371f..42f040cd766 100644 --- a/ts/features/bonus/common/store/selectors/index.ts +++ b/ts/features/bonus/common/store/selectors/index.ts @@ -8,7 +8,7 @@ import { GlobalState } from "../../../../../store/reducers/types"; import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; import { BonusVisibilityEnum } from "../../../../../../definitions/content/BonusVisibility"; -import { servicesByIdSelector } from "../../../../services/details/store/reducers/servicesById"; +import { servicesByIdSelector } from "../../../../services/details/store/reducers"; import { mapBonusIdFeatureFlag } from "../../utils"; import { AvailableBonusTypesState } from "../reducers/availableBonusesTypes"; diff --git a/ts/features/fci/hooks/useFciCheckService.tsx b/ts/features/fci/hooks/useFciCheckService.tsx index 5e10b986c5d..098fa81a1ed 100644 --- a/ts/features/fci/hooks/useFciCheckService.tsx +++ b/ts/features/fci/hooks/useFciCheckService.tsx @@ -11,7 +11,7 @@ import { fciStartSigningRequest } from "../store/actions"; import { upsertServicePreference } from "../../services/details/store/actions/preference"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; import { isServicePreferenceResponseSuccess } from "../../services/details/types/ServicePreferenceResponse"; -import { servicePreferenceSelector } from "../../services/details/store/reducers/servicePreference"; +import { servicePreferencePotSelector } from "../../services/details/store/reducers"; import { fciMetadataServiceIdSelector } from "../store/reducers/fciMetadata"; import { trackFciUxConversion } from "../analytics"; import { useIOBottomSheetModal } from "../../../utils/hooks/bottomSheet"; @@ -23,9 +23,9 @@ import { fciEnvironmentSelector } from "../store/reducers/fciEnvironment"; export const useFciCheckService = () => { const dispatch = useIODispatch(); const fciServiceId = useIOSelector(fciMetadataServiceIdSelector); - const servicePreference = useIOSelector(servicePreferenceSelector); + const servicePreferencePot = useIOSelector(servicePreferencePotSelector); const fciEnvironment = useIOSelector(fciEnvironmentSelector); - const servicePreferenceValue = pot.getOrElse(servicePreference, undefined); + const servicePreferenceValue = pot.getOrElse(servicePreferencePot, undefined); const cancelButtonProps: ButtonSolidProps = { onPress: () => { dispatch(fciStartSigningRequest()); diff --git a/ts/features/fci/screens/valid/FciQtspClausesScreen.tsx b/ts/features/fci/screens/valid/FciQtspClausesScreen.tsx index cc1026b6ec7..8154a29627b 100644 --- a/ts/features/fci/screens/valid/FciQtspClausesScreen.tsx +++ b/ts/features/fci/screens/valid/FciQtspClausesScreen.tsx @@ -29,7 +29,7 @@ import { } from "../../store/reducers/fciPollFilledDocument"; import GenericErrorComponent from "../../components/GenericErrorComponent"; import LinkedText from "../../components/LinkedText"; -import { servicePreferenceSelector } from "../../../services/details/store/reducers/servicePreference"; +import { servicePreferencePotSelector } from "../../../services/details/store/reducers"; import { loadServicePreference } from "../../../services/details/store/actions/preference"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { useFciCheckService } from "../../hooks/useFciCheckService"; @@ -45,7 +45,7 @@ const FciQtspClausesScreen = () => { const dispatch = useIODispatch(); const navigation = useIONavigation(); const [clausesChecked, setClausesChecked] = React.useState(0); - const servicePreference = useIOSelector(servicePreferenceSelector); + const servicePreferencePot = useIOSelector(servicePreferencePotSelector); const qtspClausesSelector = useIOSelector(fciQtspClausesSelector); const qtspPrivacyTextSelector = useIOSelector(fciQtspPrivacyTextSelector); const qtspPrivacyUrlSelector = useIOSelector(fciQtspPrivacyUrlSelector); @@ -58,7 +58,7 @@ const FciQtspClausesScreen = () => { const fciServiceId = useIOSelector(fciMetadataServiceIdSelector); const fciEnvironment = useIOSelector(fciEnvironmentSelector); - const servicePreferenceValue = pot.getOrElse(servicePreference, undefined); + const servicePreferenceValue = pot.getOrElse(servicePreferencePot, undefined); const isServiceActive = servicePreferenceValue && diff --git a/ts/features/fims/components/FimsSuccessBody.tsx b/ts/features/fims/components/FimsSuccessBody.tsx index 932a88d2203..dca4643d0b3 100644 --- a/ts/features/fims/components/FimsSuccessBody.tsx +++ b/ts/features/fims/components/FimsSuccessBody.tsx @@ -27,7 +27,7 @@ import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { useIOBottomSheetModal } from "../../../utils/hooks/bottomSheet"; import { openWebUrl } from "../../../utils/url"; import { loadServiceDetail } from "../../services/details/store/actions/details"; -import { serviceByIdSelector } from "../../services/details/store/reducers/servicesById"; +import { serviceByIdSelector } from "../../services/details/store/reducers"; import { logoForService } from "../../services/home/utils"; import { fimsGetRedirectUrlAndOpenIABAction } from "../store/actions"; import { ConsentData, FimsClaimType } from "../types"; diff --git a/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx index 60122d88c98..efabe0af636 100644 --- a/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx @@ -22,7 +22,7 @@ import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; -import { serviceByIdPotSelector } from "../../../services/details/store/reducers/servicesById"; +import { serviceByIdPotSelector } from "../../../services/details/store/reducers"; import { getPDNDCriteriaDescription } from "../utils/strings"; import { useOnboardingMachineService } from "../xstate/provider"; import { pdndCriteriaSelector, selectServiceId } from "../xstate/selectors"; diff --git a/ts/features/messages/components/Home/__tests__/homeUtils.test.ts b/ts/features/messages/components/Home/__tests__/homeUtils.test.ts index fad36a41688..10e12a90836 100644 --- a/ts/features/messages/components/Home/__tests__/homeUtils.test.ts +++ b/ts/features/messages/components/Home/__tests__/homeUtils.test.ts @@ -370,10 +370,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return undefined, defined organization fiscal code", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.none + details: { + byId: { + [serviceId]: pot.none + } } } } @@ -389,10 +391,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return undefined, undefined organization fiscal code, service pot.noneLoading", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.noneLoading + details: { + byId: { + [serviceId]: pot.noneLoading + } } } } @@ -407,10 +411,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return undefined, undefined organization fiscal code, service pot.someLoading", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.someLoading({}) + details: { + byId: { + [serviceId]: pot.someLoading({}) + } } } } @@ -425,9 +431,11 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return loadServiceDetail.request, undefined organization fiscal code, service unmatching", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: {} + details: { + byId: {} + } } } } as GlobalState; @@ -442,10 +450,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return loadServiceDetail.request, undefined organization fiscal code, service pot.none", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.none + details: { + byId: { + [serviceId]: pot.none + } } } } @@ -461,10 +471,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return loadServiceDetail.request, undefined organization fiscal code, service pot.noneUpdating", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.noneUpdating({}) + details: { + byId: { + [serviceId]: pot.noneUpdating({}) + } } } } @@ -480,10 +492,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return loadServiceDetail.request, undefined organization fiscal code, service pot.noneError", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.noneError(new Error()) + details: { + byId: { + [serviceId]: pot.noneError(new Error()) + } } } } @@ -499,10 +513,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return loadServiceDetail.request, undefined organization fiscal code, service pot.some", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.some({}) + details: { + byId: { + [serviceId]: pot.some({}) + } } } } @@ -518,10 +534,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return loadServiceDetail.request, undefined organization fiscal code, service pot.someUpdating", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.someUpdating({}, {}) + details: { + byId: { + [serviceId]: pot.someUpdating({}, {}) + } } } } @@ -537,10 +555,12 @@ describe("getLoadServiceDetailsActionIfNeeded", () => { it("should return loadServiceDetail.request, undefined organization fiscal code, service pot.someError", () => { const serviceId = "01HYE2HRFESQ9TN5E1WZ99AW8Z" as ServiceId; const globalState = { - entities: { + features: { services: { - byId: { - [serviceId]: pot.someError({}, new Error()) + details: { + byId: { + [serviceId]: pot.someError({}, new Error()) + } } } } diff --git a/ts/features/messages/components/Home/homeUtils.ts b/ts/features/messages/components/Home/homeUtils.ts index df16176963b..c3898dc913e 100644 --- a/ts/features/messages/components/Home/homeUtils.ts +++ b/ts/features/messages/components/Home/homeUtils.ts @@ -11,7 +11,7 @@ import I18n from "../../../../i18n"; import { convertReceivedDateToAccessible } from "../../utils/convertDateToWordDistance"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { loadServiceDetail } from "../../../services/details/store/actions/details"; -import { isLoadingServiceByIdSelector } from "../../../services/details/store/reducers/servicesById"; +import { isLoadingServiceByIdSelector } from "../../../services/details/store/reducers"; import { messagePagePotFromCategorySelector, shownMessageCategorySelector diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsFooter.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsFooter.tsx index 447d84e4c8d..ff2c284bfcf 100644 --- a/ts/features/messages/components/MessageDetail/MessageDetailsFooter.tsx +++ b/ts/features/messages/components/MessageDetail/MessageDetailsFooter.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from "react"; import { StyleSheet, View } from "react-native"; import { IOColors, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; import { useIOSelector } from "../../../../store/hooks"; -import { serviceMetadataByIdSelector } from "../../../services/details/store/reducers/servicesById"; +import { serviceMetadataByIdSelector } from "../../../services/details/store/reducers"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { UIMessageId } from "../../types"; import I18n from "../../../../i18n"; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsHeader.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsHeader.tsx index fc23444f859..60bef8c2548 100644 --- a/ts/features/messages/components/MessageDetail/MessageDetailsHeader.tsx +++ b/ts/features/messages/components/MessageDetail/MessageDetailsHeader.tsx @@ -7,9 +7,9 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { localeDateFormat } from "../../../../utils/locale"; import I18n from "../../../../i18n"; -import { logosForService } from "../../../../utils/services"; +import { logosForService } from "../../../services/common/utils"; import { useIOSelector } from "../../../../store/hooks"; -import { serviceByIdPotSelector } from "../../../services/details/store/reducers/servicesById"; +import { serviceByIdPotSelector } from "../../../services/details/store/reducers"; import { gapBetweenItemsInAGrid } from "../../utils"; import { OrganizationHeader } from "./OrganizationHeader"; diff --git a/ts/features/messages/saga/__test__/handleLoadMessageData.test.ts b/ts/features/messages/saga/__test__/handleLoadMessageData.test.ts index b4b0726ef98..5db69f1c4b5 100644 --- a/ts/features/messages/saga/__test__/handleLoadMessageData.test.ts +++ b/ts/features/messages/saga/__test__/handleLoadMessageData.test.ts @@ -12,7 +12,7 @@ import { upsertMessageStatusAttributes } from "../../store/actions"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import { serviceByIdPotSelector } from "../../../services/details/store/reducers/servicesById"; +import { serviceByIdPotSelector } from "../../../services/details/store/reducers"; import { loadServiceDetail } from "../../../services/details/store/actions/details"; import { messageDetailsByIdSelector } from "../../store/reducers/detailsById"; import { ThirdPartyMessageWithContent } from "../../../../../definitions/backend/ThirdPartyMessageWithContent"; diff --git a/ts/features/messages/saga/handleLoadMessageData.ts b/ts/features/messages/saga/handleLoadMessageData.ts index 52240ef0fa4..609d6a1b64f 100644 --- a/ts/features/messages/saga/handleLoadMessageData.ts +++ b/ts/features/messages/saga/handleLoadMessageData.ts @@ -18,7 +18,7 @@ import { } from "../store/actions"; import { getPaginatedMessageById } from "../store/reducers/paginatedById"; import { UIMessage, UIMessageDetails, UIMessageId } from "../types"; -import { serviceByIdPotSelector } from "../../services/details/store/reducers/servicesById"; +import { serviceByIdPotSelector } from "../../services/details/store/reducers"; import { loadServiceDetail } from "../../services/details/store/actions/details"; import { messageDetailsByIdSelector } from "../store/reducers/detailsById"; import { thirdPartyFromIdSelector } from "../store/reducers/thirdPartyById"; diff --git a/ts/features/messages/screens/MessageDetailsScreen.tsx b/ts/features/messages/screens/MessageDetailsScreen.tsx index f31db11524e..4c0bad7d722 100644 --- a/ts/features/messages/screens/MessageDetailsScreen.tsx +++ b/ts/features/messages/screens/MessageDetailsScreen.tsx @@ -40,7 +40,7 @@ import { cancelPaymentStatusTracking } from "../../pn/store/actions"; import { userSelectedPaymentRptIdSelector } from "../store/reducers/payments"; import { MessageDetailsStickyFooter } from "../components/MessageDetail/MessageDetailsStickyFooter"; import { MessageDetailsScrollViewAdditionalSpace } from "../components/MessageDetail/MessageDetailsScrollViewAdditionalSpace"; -import { serviceMetadataByIdSelector } from "../../services/details/store/reducers/servicesById"; +import { serviceMetadataByIdSelector } from "../../services/details/store/reducers"; import { isPNOptInMessage } from "../../pn/utils"; import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; import { diff --git a/ts/features/messages/screens/legacy/LegacyMessageDetailScreen.tsx b/ts/features/messages/screens/legacy/LegacyMessageDetailScreen.tsx index 4aab6057158..99d94ebc4c5 100644 --- a/ts/features/messages/screens/legacy/LegacyMessageDetailScreen.tsx +++ b/ts/features/messages/screens/legacy/LegacyMessageDetailScreen.tsx @@ -36,7 +36,7 @@ import { UIMessageId } from "../../types"; import { serviceByIdPotSelector, serviceMetadataByIdSelector -} from "../../../services/details/store/reducers/servicesById"; +} from "../../../services/details/store/reducers"; import { toUIService } from "../../../../store/reducers/entities/services/transformers"; import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; diff --git a/ts/features/pn/components/LegacyServiceCTA.tsx b/ts/features/pn/components/LegacyServiceCTA.tsx index f6bd24a2a6b..bcb2d9a7fa9 100644 --- a/ts/features/pn/components/LegacyServiceCTA.tsx +++ b/ts/features/pn/components/LegacyServiceCTA.tsx @@ -11,7 +11,7 @@ import { LoadingIndicator } from "../../../components/ui/LoadingIndicator"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; import I18n from "../../../i18n"; import { useIODispatch, useIOSelector } from "../../../store/hooks"; -import { servicePreferenceSelector } from "../../services/details/store/reducers/servicePreference"; +import { servicePreferencePotSelector } from "../../services/details/store/reducers"; import { isServicePreferenceResponseSuccess } from "../../services/details/types/ServicePreferenceResponse"; import { AppDispatch } from "../../../App"; import { pnActivationUpsert } from "../store/actions"; @@ -61,11 +61,11 @@ const LegacyPnServiceCTA = ({ serviceId, activate }: Props) => { const dispatch = useIODispatch(); const serviceActivation = useIOSelector(pnActivationSelector); - const servicePreference = useIOSelector(servicePreferenceSelector); - const servicePreferenceValue = pot.getOrElse(servicePreference, undefined); + const servicePreferencePot = useIOSelector(servicePreferencePotSelector); + const servicePreferenceValue = pot.getOrElse(servicePreferencePot, undefined); const isLoading = - pot.isLoading(servicePreference) || + pot.isLoading(servicePreferencePot) || pot.isLoading(serviceActivation) || pot.isUpdating(serviceActivation); diff --git a/ts/features/pn/components/MessageBottomMenu.tsx b/ts/features/pn/components/MessageBottomMenu.tsx index cf0db7b4df2..9f2d62ce7d4 100644 --- a/ts/features/pn/components/MessageBottomMenu.tsx +++ b/ts/features/pn/components/MessageBottomMenu.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from "react"; import { StyleSheet, View } from "react-native"; import { ContactsListItem } from "../../messages/components/MessageDetail/ContactsListItem"; import { useIOSelector } from "../../../store/hooks"; -import { serviceMetadataByIdSelector } from "../../services/details/store/reducers/servicesById"; +import { serviceMetadataByIdSelector } from "../../services/details/store/reducers"; import { ServiceId } from "../../../../definitions/backend/ServiceId"; import { ShowMoreListItem, diff --git a/ts/features/pn/components/MessageDetailsHeader.tsx b/ts/features/pn/components/MessageDetailsHeader.tsx index d46ca6acd70..fbd272008dc 100644 --- a/ts/features/pn/components/MessageDetailsHeader.tsx +++ b/ts/features/pn/components/MessageDetailsHeader.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; import OrganizationHeader from "../../../components/OrganizationHeader"; -import { logosForService } from "../../../utils/services"; +import { logosForService } from "../../services/common/utils"; type Props = Readonly<{ service: ServicePublic }>; diff --git a/ts/features/pn/components/ServiceCTA.tsx b/ts/features/pn/components/ServiceCTA.tsx index 7d7e43464fb..93b06943bd1 100644 --- a/ts/features/pn/components/ServiceCTA.tsx +++ b/ts/features/pn/components/ServiceCTA.tsx @@ -7,9 +7,9 @@ import { ServiceId } from "../../../../definitions/backend/ServiceId"; import I18n from "../../../i18n"; import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { - servicePreferenceResponseSuccessSelector, - servicePreferenceSelector -} from "../../services/details/store/reducers/servicePreference"; + servicePreferencePotSelector, + servicePreferenceResponseSuccessSelector +} from "../../services/details/store/reducers"; import { pnActivationUpsert } from "../store/actions"; import { isLoadingPnActivationSelector } from "../store/reducers/activation"; import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; @@ -32,7 +32,7 @@ export const PnServiceCta = ({ serviceId, activate }: PnServiceCtaProps) => { servicePreferenceResponseSuccessSelector ); - const servicePreferencePot = useIOSelector(servicePreferenceSelector); + const servicePreferencePot = useIOSelector(servicePreferencePotSelector); const isLoadingPnActivation = useIOSelector(isLoadingPnActivationSelector); diff --git a/ts/features/pn/screens/LegacyMessageDetailsScreen.tsx b/ts/features/pn/screens/LegacyMessageDetailsScreen.tsx index c0684526fc2..541c66d875f 100644 --- a/ts/features/pn/screens/LegacyMessageDetailsScreen.tsx +++ b/ts/features/pn/screens/LegacyMessageDetailsScreen.tsx @@ -9,7 +9,7 @@ import BaseScreenComponent from "../../../components/screens/BaseScreenComponent import I18n from "../../../i18n"; import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector, useIOStore } from "../../../store/hooks"; -import { serviceByIdPotSelector } from "../../services/details/store/reducers/servicesById"; +import { serviceByIdSelector } from "../../services/details/store/reducers"; import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; import { LegacyMessageDetails } from "../components/LegacyMessageDetails"; @@ -47,9 +47,7 @@ export const LegacyMessageDetailsScreen = ( const dispatch = useIODispatch(); const navigation = useNavigation(); - const service = pot.toUndefined( - useIOSelector(state => serviceByIdPotSelector(state, serviceId)) - ); + const service = useIOSelector(state => serviceByIdSelector(state, serviceId)); const currentFiscalCode = useIOSelector(profileFiscalCodeSelector); const messagePot = useIOSelector(state => diff --git a/ts/features/pn/store/sagas/watchPnSaga.ts b/ts/features/pn/store/sagas/watchPnSaga.ts index 7752be83d76..551578cd486 100644 --- a/ts/features/pn/store/sagas/watchPnSaga.ts +++ b/ts/features/pn/store/sagas/watchPnSaga.ts @@ -16,7 +16,7 @@ import { trackPNServiceStatusChangeError, trackPNServiceStatusChangeSuccess } from "../../analytics"; -import { servicePreferenceSelector } from "../../../services/details/store/reducers/servicePreference"; +import { servicePreferencePotSelector } from "../../../services/details/store/reducers"; import { isServicePreferenceResponseSuccess } from "../../../services/details/types/ServicePreferenceResponse"; import { watchPaymentStatusForMixpanelTracking } from "./watchPaymentStatusSaga"; @@ -62,7 +62,9 @@ function* handlePnActivation( } function* reportPNServiceStatusOnFailure(predictedValue: boolean) { - const selectedServicePreferencePot = yield* select(servicePreferenceSelector); + const selectedServicePreferencePot = yield* select( + servicePreferencePotSelector + ); const isServiceActive = pipe( selectedServicePreferencePot, pot.toOption, diff --git a/ts/features/services/common/store/reducers/index.ts b/ts/features/services/common/store/reducers/index.ts index 53ac7faa9aa..7a007fc7a8c 100644 --- a/ts/features/services/common/store/reducers/index.ts +++ b/ts/features/services/common/store/reducers/index.ts @@ -4,14 +4,19 @@ import institutionReducer, { InstitutionState } from "../../../institution/store/reducers"; import searchReducer, { SearchState } from "../../../search/store/reducers"; +import servicesDetailsReducer, { + ServicesDetailsState +} from "../../../details/store/reducers"; export type ServicesState = { + details: ServicesDetailsState; home: ServicesHomeState; institution: InstitutionState; search: SearchState; }; const servicesReducer = combineReducers({ + details: servicesDetailsReducer, home: homeReducer, institution: institutionReducer, search: searchReducer diff --git a/ts/features/services/common/utils/index.ts b/ts/features/services/common/utils/index.ts index 58baab0efc2..754e1807d6f 100644 --- a/ts/features/services/common/utils/index.ts +++ b/ts/features/services/common/utils/index.ts @@ -1,3 +1,5 @@ +import { ImageURISource } from "react-native"; +import { ServicePublic } from "../../../../../definitions/backend/ServicePublic"; import { contentRepoUrl } from "../../../../config"; export function getLogoForInstitution( @@ -8,3 +10,22 @@ export function getLogoForInstitution( uri: `${logosRepoUrl}/${u}.png` })); } + +/** + * Returns an array of ImageURISource pointing to possible logos for the + * provided service. + * + * The returned array is suitable for being used with the MultiImage component. + * The arrays will have first the service logo, then the organization logo. + */ +export function logosForService( + service: ServicePublic, + logosRepoUrl: string = `${contentRepoUrl}/logos` +): ReadonlyArray { + return [ + `services/${service.service_id.toLowerCase()}`, + `organizations/${service.organization_fiscal_code.replace(/^0+/, "")}` + ].map(u => ({ + uri: `${logosRepoUrl}/${u}.png` + })); +} diff --git a/ts/features/services/details/components/ServiceDetailsMetadata.tsx b/ts/features/services/details/components/ServiceDetailsMetadata.tsx index 5dfa0749ad7..9ca51cbb301 100644 --- a/ts/features/services/details/components/ServiceDetailsMetadata.tsx +++ b/ts/features/services/details/components/ServiceDetailsMetadata.tsx @@ -12,7 +12,7 @@ import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { ServiceMetadata } from "../../../../../definitions/backend/ServiceMetadata"; import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; -import { serviceMetadataByIdSelector } from "../store/reducers/servicesById"; +import { serviceMetadataByIdSelector } from "../store/reducers"; import { handleItemOnPress } from "../../../../utils/url"; import * as analytics from "../../common/analytics"; diff --git a/ts/features/services/details/components/ServiceDetailsPreferences.tsx b/ts/features/services/details/components/ServiceDetailsPreferences.tsx index df3f5b7b00a..b8ef55afa99 100644 --- a/ts/features/services/details/components/ServiceDetailsPreferences.tsx +++ b/ts/features/services/details/components/ServiceDetailsPreferences.tsx @@ -23,9 +23,9 @@ import { upsertServicePreference } from "../store/actions/preference"; import { isErrorServicePreferenceSelector, isLoadingServicePreferenceSelector, + serviceMetadataInfoSelector, servicePreferenceResponseSuccessSelector -} from "../store/reducers/servicePreference"; -import { serviceMetadataInfoSelector } from "../store/reducers/servicesById"; +} from "../store/reducers"; const hasChannel = ( notificationChannel: NotificationChannelEnum, diff --git a/ts/features/services/details/components/ServiceDetailsTosAndPrivacy.tsx b/ts/features/services/details/components/ServiceDetailsTosAndPrivacy.tsx index 509ce921bdf..3c92dc62fef 100644 --- a/ts/features/services/details/components/ServiceDetailsTosAndPrivacy.tsx +++ b/ts/features/services/details/components/ServiceDetailsTosAndPrivacy.tsx @@ -10,7 +10,7 @@ import { import I18n from "../../../../i18n"; import { openWebUrl } from "../../../../utils/url"; import { useIOSelector } from "../../../../store/hooks"; -import { serviceMetadataByIdSelector } from "../store/reducers/servicesById"; +import { serviceMetadataByIdSelector } from "../store/reducers"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; type TosAndPrivacyListItem = { diff --git a/ts/features/services/details/saga/__tests__/handleServiceDetails.test.ts b/ts/features/services/details/saga/__tests__/handleServiceDetails.test.ts index f06cca203e5..a57259834cf 100644 --- a/ts/features/services/details/saga/__tests__/handleServiceDetails.test.ts +++ b/ts/features/services/details/saga/__tests__/handleServiceDetails.test.ts @@ -8,8 +8,6 @@ import { ServiceName } from "../../../../../../definitions/backend/ServiceName"; import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; import { loadServiceDetail } from "../../store/actions/details"; import { handleServiceDetails } from "../handleServiceDetails"; -import { handleOrganizationNameUpdateSaga } from "../../../../../sagas/services/handleOrganizationNameUpdateSaga"; -import { handleServiceReadabilitySaga } from "../../../../../sagas/services/handleServiceReadabilitySaga"; import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; import { BackendClient } from "../../../../../api/__mocks__/backend"; @@ -64,10 +62,6 @@ describe("handleServiceDetails", () => { .next(E.right({ status: 200, value: mockedService })) .put(loadServiceDetail.success(mockedService)) .next() - .call(handleServiceReadabilitySaga, mockedServiceId) - .next() - .call(handleOrganizationNameUpdateSaga, mockedService) - .next() .isDone(); }); }); diff --git a/ts/features/services/details/saga/handleServiceDetails.ts b/ts/features/services/details/saga/handleServiceDetails.ts index 8f08e6e357b..e87b90e389f 100644 --- a/ts/features/services/details/saga/handleServiceDetails.ts +++ b/ts/features/services/details/saga/handleServiceDetails.ts @@ -3,15 +3,11 @@ import { call, put } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; import { PathTraversalSafePathParam } from "../../../../../definitions/backend/PathTraversalSafePathParam"; import { BackendClient } from "../../../../api/backend"; -import { handleOrganizationNameUpdateSaga } from "../../../../sagas/services/handleOrganizationNameUpdateSaga"; -import { handleServiceReadabilitySaga } from "../../../../sagas/services/handleServiceReadabilitySaga"; -import { loadServiceDetailNotFound } from "../../../../store/actions/services"; import { SagaCallReturnType } from "../../../../types/utils"; import { convertUnknownToError } from "../../../../utils/errors"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; import { loadServiceDetail } from "../store/actions/details"; import { readablePrivacyReport } from "../../../../utils/reporters"; -import { ServiceId } from "../../../../../definitions/backend/ServiceId"; /** * saga to handle the loading of a service detail @@ -50,20 +46,8 @@ export function* handleServiceDetails( if (response.right.status === 200) { yield* put(loadServiceDetail.success(response.right.value)); - // If it is occurring during the first load of serivces, - // mark the service as read (it will not display the badge on the list item) - yield* call(handleServiceReadabilitySaga, action.payload); - // Update, if needed, the name of the organization that provides the service - yield* call(handleOrganizationNameUpdateSaga, response.right.value); - return; } - - if (response.right.status === 404) { - yield* put( - loadServiceDetailNotFound(action.payload as unknown as ServiceId) - ); - } // not handled error codes yield* put( loadServiceDetail.failure({ diff --git a/ts/features/services/details/saga/handleUpsertServicePreference.ts b/ts/features/services/details/saga/handleUpsertServicePreference.ts index 1119bf45081..8dad65bc68b 100644 --- a/ts/features/services/details/saga/handleUpsertServicePreference.ts +++ b/ts/features/services/details/saga/handleUpsertServicePreference.ts @@ -14,10 +14,9 @@ import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; import { trackPNPushSettings } from "../../../pn/analytics"; import { upsertServicePreference } from "../store/actions/preference"; import { - ServicePreferenceState, - servicePreferenceSelector -} from "../store/reducers/servicePreference"; -import { serviceMetadataInfoSelector } from "../store/reducers/servicesById"; + serviceMetadataInfoSelector, + servicePreferencePotSelector +} from "../store/reducers"; import { isServicePreferenceResponseSuccess } from "../types/ServicePreferenceResponse"; import { mapKinds } from "./handleGetServicePreference"; @@ -29,7 +28,9 @@ import { mapKinds } from "./handleGetServicePreference"; * @param action */ const calculateUpdatingPreference = ( - currentServicePreferenceState: ServicePreferenceState, + currentServicePreferenceState: ReturnType< + typeof servicePreferencePotSelector + >, action: ActionType ): ServicePreference => { if ( @@ -96,11 +97,10 @@ export function* handleUpsertServicePreference( ) { yield* call(trackPNPushNotificationSettings, action); - const currentPreferences: ReturnType = - yield* select(servicePreferenceSelector); + const servicePreferencePot = yield* select(servicePreferencePotSelector); const updatingPreference = calculateUpdatingPreference( - currentPreferences, + servicePreferencePot, action ); diff --git a/ts/features/services/details/screens/ServiceDetailsScreen.tsx b/ts/features/services/details/screens/ServiceDetailsScreen.tsx index dc9ccbc9fca..89fac31662f 100644 --- a/ts/features/services/details/screens/ServiceDetailsScreen.tsx +++ b/ts/features/services/details/screens/ServiceDetailsScreen.tsx @@ -8,7 +8,7 @@ import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; -import { logosForService } from "../../../../utils/services"; +import { logosForService } from "../../common/utils"; import { CTA, CTAS } from "../../../messages/types/MessageCTA"; import { getServiceCTA, @@ -39,7 +39,7 @@ import { serviceByIdSelector, serviceMetadataByIdSelector, serviceMetadataInfoSelector -} from "../store/reducers/servicesById"; +} from "../store/reducers"; import { ServiceMetadataInfo } from "../types/ServiceMetadataInfo"; export type ServiceDetailsScreenRouteParams = { diff --git a/ts/features/services/details/store/reducers/__tests__/servicePreference.test.ts b/ts/features/services/details/store/reducers/__tests__/servicePreference.test.ts index b7bef3bc180..c8ca87f5fc3 100644 --- a/ts/features/services/details/store/reducers/__tests__/servicePreference.test.ts +++ b/ts/features/services/details/store/reducers/__tests__/servicePreference.test.ts @@ -17,7 +17,7 @@ import { isErrorServicePreferenceSelector, isLoadingServicePreferenceSelector, servicePreferenceResponseSuccessSelector -} from "../servicePreference"; +} from ".."; import { GlobalState } from "../../../../../../store/reducers/types"; const serviceId = "serviceId" as ServiceId; @@ -57,7 +57,9 @@ describe("servicePreference reducer", () => { it("should have initial state", () => { const state = appReducer(undefined, applicationChangeState("active")); - expect(state.entities.services.servicePreference).toStrictEqual(pot.none); + expect(state.features.services.details.servicePreference).toStrictEqual( + pot.none + ); }); it("should handle loadServicePreference action", () => { @@ -66,19 +68,21 @@ describe("servicePreference reducer", () => { store.dispatch(loadServicePreference.request(serviceId)); - expect(store.getState().entities.services.servicePreference).toStrictEqual( - pot.noneLoading - ); + expect( + store.getState().features.services.details.servicePreference + ).toStrictEqual(pot.noneLoading); store.dispatch( loadServicePreference.success(servicePreferenceResponseSuccess) ); - expect(store.getState().entities.services.servicePreference).toStrictEqual( - pot.some(servicePreferenceResponseSuccess) - ); + expect( + store.getState().features.services.details.servicePreference + ).toStrictEqual(pot.some(servicePreferenceResponseSuccess)); store.dispatch(loadServicePreference.failure(servicePreferenceError)); - expect(store.getState().entities.services.servicePreference).toStrictEqual( + expect( + store.getState().features.services.details.servicePreference + ).toStrictEqual( pot.someError(servicePreferenceResponseSuccess, servicePreferenceError) ); }); @@ -87,11 +91,14 @@ describe("servicePreference reducer", () => { const state = appReducer(undefined, applicationChangeState("active")); const finalState: GlobalState = { ...state, - entities: { - ...state.entities, + features: { + ...state.features, services: { - ...state.entities.services, - servicePreference: pot.some(servicePreferenceResponseSuccess) + ...state.features.services, + details: { + ...state.features.services.details, + servicePreference: pot.some(servicePreferenceResponseSuccess) + } } } }; @@ -99,7 +106,9 @@ describe("servicePreference reducer", () => { store.dispatch(upsertServicePreference.request(updatingResponse)); - expect(store.getState().entities.services.servicePreference).toStrictEqual( + expect( + store.getState().features.services.details.servicePreference + ).toStrictEqual( pot.someUpdating(servicePreferenceResponseSuccess, { id: serviceId, kind: "success", @@ -114,7 +123,9 @@ describe("servicePreference reducer", () => { ); store.dispatch(upsertServicePreference.failure(servicePreferenceError)); - expect(store.getState().entities.services.servicePreference).toStrictEqual( + expect( + store.getState().features.services.details.servicePreference + ).toStrictEqual( pot.someError(servicePreferenceResponseSuccess, servicePreferenceError) ); }); diff --git a/ts/features/services/details/store/reducers/__tests__/servicesById.test.ts b/ts/features/services/details/store/reducers/__tests__/servicesById.test.ts index b4c22e27de1..9537c37e4c5 100644 --- a/ts/features/services/details/store/reducers/__tests__/servicesById.test.ts +++ b/ts/features/services/details/store/reducers/__tests__/servicesById.test.ts @@ -3,7 +3,6 @@ import { NonEmptyString, OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; -import { Tuple2 } from "@pagopa/ts-commons/lib/tuples"; import { Action, createStore } from "redux"; import { ServiceId } from "../../../../../../../definitions/backend/ServiceId"; import { ServicePublic } from "../../../../../../../definitions/backend/ServicePublic"; @@ -16,7 +15,6 @@ import { sessionExpired } from "../../../../../../store/actions/authentication"; import { loadServiceDetail } from "../../actions/details"; -import { removeServiceTuples } from "../../../../../../store/actions/services"; import { appReducer } from "../../../../../../store/reducers"; import { GlobalState } from "../../../../../../store/reducers/types"; import { reproduceSequence } from "../../../../../../utils/tests"; @@ -26,7 +24,7 @@ import { serviceByIdSelector, serviceMetadataByIdSelector, serviceMetadataInfoSelector -} from "../servicesById"; +} from ".."; const serviceId = "serviceId" as ServiceId; @@ -42,7 +40,7 @@ describe("serviceById reducer", () => { it("should have initial state", () => { const state = appReducer(undefined, applicationChangeState("active")); - expect(state.entities.services.byId).toStrictEqual({}); + expect(state.features.services.details.byId).toStrictEqual({}); }); it("should handle loadServiceDetail action", () => { @@ -51,12 +49,12 @@ describe("serviceById reducer", () => { store.dispatch(loadServiceDetail.request(serviceId)); - expect(store.getState().entities.services.byId).toStrictEqual({ + expect(store.getState().features.services.details.byId).toStrictEqual({ serviceId: pot.noneLoading }); store.dispatch(loadServiceDetail.success(service)); - expect(store.getState().entities.services.byId).toStrictEqual({ + expect(store.getState().features.services.details.byId).toStrictEqual({ serviceId: pot.some(service) }); @@ -66,7 +64,7 @@ describe("serviceById reducer", () => { }; store.dispatch(loadServiceDetail.failure(tError)); - expect(store.getState().entities.services.byId).toStrictEqual({ + expect(store.getState().features.services.details.byId).toStrictEqual({ serviceId: pot.someError(service, new Error("load failed")) }); }); @@ -83,7 +81,7 @@ describe("serviceById reducer", () => { appReducer, sequenceOfActions ); - expect(state.entities.services.byId).toEqual({}); + expect(state.features.services.details.byId).toEqual({}); }); it("should handle sessionExpired action", () => { @@ -98,36 +96,7 @@ describe("serviceById reducer", () => { appReducer, sequenceOfActions ); - expect(state.entities.services.byId).toEqual({}); - }); - - it("should handle removeServiceTuples action", () => { - const sequenceOfActions: ReadonlyArray = [ - applicationChangeState("active"), - loadServiceDetail.success({ ...service, service_id: "s1" as ServiceId }), - loadServiceDetail.success({ ...service, service_id: "s2" as ServiceId }), - loadServiceDetail.success({ ...service, service_id: "s3" as ServiceId }), - loadServiceDetail.success({ ...service, service_id: "s4" as ServiceId }), - loadServiceDetail.success({ ...service, service_id: "s5" as ServiceId }), - removeServiceTuples([ - Tuple2("s2", "FSCLCD"), - Tuple2("s3", "FSCLCD"), - // Not existing serviceId - Tuple2("s6", "FSCLCD") - ]) - ]; - - const state: GlobalState = reproduceSequence( - {} as GlobalState, - appReducer, - sequenceOfActions - ); - - expect(state.entities.services.byId).toEqual({ - s1: pot.some({ ...service, service_id: "s1" as ServiceId }), - s4: pot.some({ ...service, service_id: "s4" as ServiceId }), - s5: pot.some({ ...service, service_id: "s5" as ServiceId }) - }); + expect(state.features.services.details.byId).toEqual({}); }); }); diff --git a/ts/features/services/details/store/reducers/index.ts b/ts/features/services/details/store/reducers/index.ts new file mode 100644 index 00000000000..2628ac8aa12 --- /dev/null +++ b/ts/features/services/details/store/reducers/index.ts @@ -0,0 +1,214 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import { createSelector } from "reselect"; +import { getType } from "typesafe-actions"; +import { Action } from "../../../../../store/actions/types"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { NetworkError } from "../../../../../utils/errors"; +import { isStrictSome } from "../../../../../utils/pot"; +import { + ServicePreferenceResponse, + WithServiceID, + isServicePreferenceResponseSuccess +} from "../../types/ServicePreferenceResponse"; +import { + loadServicePreference, + upsertServicePreference +} from "../actions/preference"; +import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; +import { loadServiceDetail } from "../actions/details"; +import { + logoutSuccess, + sessionExpired +} from "../../../../../store/actions/authentication"; +import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; +import { SpecialServiceMetadata } from "../../../../../../definitions/backend/SpecialServiceMetadata"; + +export type ServicesDetailsState = { + byId: Record>; + servicePreference: pot.Pot< + ServicePreferenceResponse, + WithServiceID + >; +}; + +const INITIAL_STATE: ServicesDetailsState = { + byId: {}, + servicePreference: pot.none +}; + +const servicesDetailsReducer = ( + state: ServicesDetailsState = INITIAL_STATE, + action: Action +): ServicesDetailsState => { + switch (action.type) { + // Get service details actions + case getType(loadServiceDetail.request): + // When a previously loaded service detail is loaded again, its state + // is updated with a someLoading pot, otherwise its state is updated with a noneLoading pot + return { + ...state, + byId: { + ...state.byId, + [action.payload]: pipe( + state.byId[action.payload], + O.fromNullable, + O.fold(() => pot.noneLoading, pot.toLoading) + ) + } + }; + + case getType(loadServiceDetail.success): + // Use the ID as object key + return { + ...state, + byId: { + ...state.byId, + [action.payload.service_id]: pot.some(action.payload) + } + }; + + case getType(loadServiceDetail.failure): + // when a request to load a previously loaded service detail fails its state is updated + // with a someError pot, otherwise its state is updated with a noneError pot + return { + ...state, + byId: { + ...state.byId, + [action.payload.service_id]: pipe( + state.byId[action.payload.service_id], + O.fromNullable, + O.fold( + () => pot.noneError(action.payload.error), + servicePot => pot.toError(servicePot, action.payload.error) + ) + ) + } + }; + + // Get service preference actions + case getType(loadServicePreference.request): + return { + ...state, + servicePreference: pot.toLoading(state.servicePreference) + }; + case getType(upsertServicePreference.request): + const { id, ...payload } = action.payload; + + return { + ...state, + servicePreference: pot.toUpdating(state.servicePreference, { + id, + kind: "success", + value: payload + }) + }; + case getType(loadServicePreference.success): + case getType(upsertServicePreference.success): + return { + ...state, + servicePreference: pot.some(action.payload) + }; + case getType(loadServicePreference.failure): + case getType(upsertServicePreference.failure): + return { + ...state, + servicePreference: pot.toError(state.servicePreference, action.payload) + }; + + case getType(logoutSuccess): + case getType(sessionExpired): + return INITIAL_STATE; + } + return state; +}; + +export default servicesDetailsReducer; + +// Selectors +export const servicesByIdSelector = (state: GlobalState) => + state.features.services.details.byId; + +export const serviceByIdPotSelector = ( + state: GlobalState, + id: ServiceId +): pot.Pot => + state.features.services.details.byId[id] ?? pot.none; + +export const serviceByIdSelector = ( + state: GlobalState, + id: ServiceId +): ServicePublic | undefined => + pipe(serviceByIdPotSelector(state, id), pot.toUndefined); + +export const isLoadingServiceByIdSelector = ( + state: GlobalState, + id: ServiceId +) => pipe(serviceByIdPotSelector(state, id), pot.isLoading); + +export const isErrorServiceByIdSelector = (state: GlobalState, id: ServiceId) => + pipe(serviceByIdPotSelector(state, id), pot.isError); + +export const serviceMetadataByIdSelector = createSelector( + serviceByIdPotSelector, + serviceByIdPot => + pipe( + serviceByIdPot, + pot.toOption, + O.chainNullableK(service => service.service_metadata), + O.toUndefined + ) +); + +export const serviceMetadataInfoSelector = createSelector( + serviceMetadataByIdSelector, + serviceMetadata => + pipe( + serviceMetadata, + O.fromNullable, + O.chain(serviceMetadata => { + if (SpecialServiceMetadata.is(serviceMetadata)) { + return O.some({ + isSpecialService: true, + customSpecialFlow: serviceMetadata.custom_special_flow + }); + } + return O.none; + }), + O.toUndefined + ) +); + +export const servicePreferencePotSelector = (state: GlobalState) => + state.features.services.details.servicePreference; + +export const servicePreferenceResponseSuccessSelector = createSelector( + servicePreferencePotSelector, + servicePreferencePot => + pipe( + servicePreferencePot, + pot.toOption, + O.filter(isServicePreferenceResponseSuccess), + O.toUndefined + ) +); + +export const isLoadingServicePreferenceSelector = (state: GlobalState) => + pipe( + state, + servicePreferencePotSelector, + servicePreferencePot => + pot.isLoading(servicePreferencePot) || + pot.isUpdating(servicePreferencePot) + ); + +export const isErrorServicePreferenceSelector = (state: GlobalState) => + pipe( + state, + servicePreferencePotSelector, + servicePreferencePot => + pot.isError(servicePreferencePot) || + (isStrictSome(servicePreferencePot) && + !isServicePreferenceResponseSuccess(servicePreferencePot.value)) + ); diff --git a/ts/features/services/details/store/reducers/servicePreference.ts b/ts/features/services/details/store/reducers/servicePreference.ts deleted file mode 100644 index 7705976eeef..00000000000 --- a/ts/features/services/details/store/reducers/servicePreference.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { createSelector } from "reselect"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import { getType } from "typesafe-actions"; -import { - ServicePreferenceResponse, - WithServiceID, - isServicePreferenceResponseSuccess -} from "../../types/ServicePreferenceResponse"; -import { NetworkError } from "../../../../../utils/errors"; -import { isStrictSome } from "../../../../../utils/pot"; -import { - loadServicePreference, - upsertServicePreference -} from "../actions/preference"; -import { Action } from "../../../../../store/actions/types"; -import { GlobalState } from "../../../../../store/reducers/types"; - -export type ServicePreferenceState = pot.Pot< - ServicePreferenceResponse, - WithServiceID ->; - -const INITIAL_STATE: ServicePreferenceState = pot.none; - -/** - * Reducer to handle specific service contact preferences (inbox, push, emails) - */ -const servicePreferenceReducer = ( - state: ServicePreferenceState = INITIAL_STATE, - action: Action -): ServicePreferenceState => { - switch (action.type) { - case getType(loadServicePreference.request): - return pot.toLoading(state); - case getType(upsertServicePreference.request): - const { id, ...payload } = action.payload; - - return pot.toUpdating(state, { - id, - kind: "success", - value: payload - }); - case getType(loadServicePreference.success): - case getType(upsertServicePreference.success): - return pot.some(action.payload); - case getType(loadServicePreference.failure): - case getType(upsertServicePreference.failure): - return pot.toError(state, action.payload); - } - return state; -}; - -export default servicePreferenceReducer; - -// Selectors -export const servicePreferenceSelector = ( - state: GlobalState -): ServicePreferenceState => state.entities.services.servicePreference; - -export const servicePreferenceResponseSuccessSelector = createSelector( - servicePreferenceSelector, - servicePreferencePot => - pipe( - servicePreferencePot, - pot.toOption, - O.filter(isServicePreferenceResponseSuccess), - O.toUndefined - ) -); - -export const isLoadingServicePreferenceSelector = (state: GlobalState) => - pipe( - state, - servicePreferenceSelector, - servicePreferencePot => - pot.isLoading(servicePreferencePot) || - pot.isUpdating(servicePreferencePot) - ); - -export const isErrorServicePreferenceSelector = (state: GlobalState) => - pipe( - state, - servicePreferenceSelector, - servicePreferencePot => - pot.isError(servicePreferencePot) || - (isStrictSome(servicePreferencePot) && - !isServicePreferenceResponseSuccess(servicePreferencePot.value)) - ); diff --git a/ts/features/services/details/store/reducers/servicesById.ts b/ts/features/services/details/store/reducers/servicesById.ts deleted file mode 100644 index b409a1e2914..00000000000 --- a/ts/features/services/details/store/reducers/servicesById.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { createSelector } from "reselect"; -import { getType } from "typesafe-actions"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; -import { - logoutSuccess, - sessionExpired -} from "../../../../../store/actions/authentication"; -import { removeServiceTuples } from "../../../../../store/actions/services"; -import { Action } from "../../../../../store/actions/types"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; -import { SpecialServiceMetadata } from "../../../../../../definitions/backend/SpecialServiceMetadata"; -import { loadServiceDetail } from "../actions/details"; - -export type ServicesByIdState = Readonly<{ - [key: string]: pot.Pot | undefined; -}>; - -const INITIAL_STATE: ServicesByIdState = {}; - -/** - * A reducer to store the services detail normalized by id - */ -const serviceByIdReducer = ( - state: ServicesByIdState = INITIAL_STATE, - action: Action -): ServicesByIdState => { - switch (action.type) { - case getType(loadServiceDetail.request): - // When a previously loaded service detail is loaded again, its state - // is updated with a someLoading pot, otherwise its state is updated with a noneLoading pot - return { - ...state, - [action.payload]: pipe( - state[action.payload], - O.fromNullable, - O.fold(() => pot.noneLoading, pot.toLoading) - ) - }; - - case getType(loadServiceDetail.success): - // Use the ID as object key - return { - ...state, - [action.payload.service_id]: pot.some(action.payload) - }; - - case getType(loadServiceDetail.failure): - // when a request to load a previously loaded service detail fails its state is updated - // with a someError pot, otherwise its state is updated with a noneError pot - return { - ...state, - [action.payload.service_id]: pipe( - state[action.payload.service_id], - O.fromNullable, - O.fold( - () => pot.noneError(action.payload.error), - servicePot => pot.toError(servicePot, action.payload.error) - ) - ) - }; - - case getType(removeServiceTuples): { - const serviceTuples = action.payload; - const newState = { ...state }; - // eslint-disable-next-line - serviceTuples.forEach(_ => delete newState[_.e1]); - return newState; - } - - case getType(logoutSuccess): - case getType(sessionExpired): - return INITIAL_STATE; - - default: - return state; - } -}; - -export default serviceByIdReducer; - -// Selectors -export const servicesByIdSelector = (state: GlobalState): ServicesByIdState => - state.entities.services.byId; - -export const serviceByIdPotSelector = ( - state: GlobalState, - id: ServiceId -): pot.Pot => - state.entities.services.byId[id] ?? pot.none; - -export const serviceByIdSelector = ( - state: GlobalState, - id: ServiceId -): ServicePublic | undefined => - pipe(serviceByIdPotSelector(state, id), pot.toUndefined); - -export const isLoadingServiceByIdSelector = ( - state: GlobalState, - id: ServiceId -) => pipe(serviceByIdPotSelector(state, id), pot.isLoading); - -export const isErrorServiceByIdSelector = (state: GlobalState, id: ServiceId) => - pipe(serviceByIdPotSelector(state, id), pot.isError); - -export const serviceMetadataByIdSelector = createSelector( - serviceByIdPotSelector, - serviceByIdPot => - pipe( - serviceByIdPot, - pot.toOption, - O.chainNullableK(service => service.service_metadata), - O.toUndefined - ) -); - -export const serviceMetadataInfoSelector = createSelector( - serviceMetadataByIdSelector, - serviceMetadata => - pipe( - serviceMetadata, - O.fromNullable, - O.chain(serviceMetadata => { - if (SpecialServiceMetadata.is(serviceMetadata)) { - return O.some({ - isSpecialService: true, - customSpecialFlow: serviceMetadata.custom_special_flow - }); - } - return O.none; - }), - O.toUndefined - ) -); diff --git a/ts/features/wallet/component/card/FeaturedCardCarousel.tsx b/ts/features/wallet/component/card/FeaturedCardCarousel.tsx index 571af6fc4f1..cd6d77f5604 100644 --- a/ts/features/wallet/component/card/FeaturedCardCarousel.tsx +++ b/ts/features/wallet/component/card/FeaturedCardCarousel.tsx @@ -17,7 +17,6 @@ import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; import { loadServiceDetail } from "../../../services/details/store/actions/details"; -import { showServiceDetails } from "../../../../store/actions/services"; import { Dispatch } from "../../../../store/actions/types"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { @@ -100,13 +99,11 @@ const FeaturedCardCarousel: React.FunctionComponent = (props: Props) => { // TODO: add mixpanel tracking and alert: https://pagopa.atlassian.net/browse/AP-14 IOToast.info(I18n.t("bonus.cdc.serviceEntryPoint.notAvailable")); }, - s => () => { - dispatch(showServiceDetails(s)); + s => () => navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { screen: SERVICES_ROUTES.SERVICE_DETAIL, params: { serviceId: s.service_id } - }); - } + }) ) ); } diff --git a/ts/navigation/ServicesHomeTabNavigator.tsx b/ts/navigation/ServicesHomeTabNavigator.tsx deleted file mode 100644 index c1f7d46fb8d..00000000000 --- a/ts/navigation/ServicesHomeTabNavigator.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs"; -import I18n from "i18n-js"; -import * as React from "react"; -import { Platform } from "react-native"; -import { IOColors } from "@pagopa/io-app-design-system"; -import { makeFontStyleObject } from "../components/core/fonts"; -import ServicesLocalScreen from "../screens/services/ServicesLocalScreen"; -import ServicesNationalScreen from "../screens/services/ServicesNationalScreen"; - -const Tab = createMaterialTopTabNavigator(); - -const ServicesHomeTabNavigator = () => ( - - - - -); - -export default ServicesHomeTabNavigator; diff --git a/ts/navigation/TabNavigator.tsx b/ts/navigation/TabNavigator.tsx index 34923c8aa0c..eaab740bcf6 100644 --- a/ts/navigation/TabNavigator.tsx +++ b/ts/navigation/TabNavigator.tsx @@ -194,8 +194,6 @@ export const MainTabNavigator = () => { iconNameFocused="navServicesFocused" color={color} focused={focused} - // Badge counter has been disabled - // https://www.pivotaltracker.com/story/show/176919053 /> ) }} diff --git a/ts/sagas/services/__tests__/handleFirstVisibleServiceLoadSaga.test.ts b/ts/sagas/services/__tests__/handleFirstVisibleServiceLoadSaga.test.ts deleted file mode 100644 index 52750d0da89..00000000000 --- a/ts/sagas/services/__tests__/handleFirstVisibleServiceLoadSaga.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { testSaga } from "redux-saga-test-plan"; -import { firstServiceLoadSuccess } from "../../../store/actions/services"; -import { visibleServicesDetailLoadStateSelector } from "../../../store/reducers/entities/services"; -import { isFirstVisibleServiceLoadCompletedSelector } from "../../../store/reducers/entities/services/firstServicesLoading"; -import { handleFirstVisibleServiceLoadSaga } from "../handleFirstVisibleServiceLoadSaga"; - -describe("handleFirstVisibleServiceLoadSaga", () => { - it("does nothing if the visible services loading is not completed", () => { - testSaga(handleFirstVisibleServiceLoadSaga) - .next() - .select(isFirstVisibleServiceLoadCompletedSelector) - .next(false) - .select(visibleServicesDetailLoadStateSelector) - .next(pot.noneLoading) - .isDone(); - }); - - it("saves on the redux store that the first service loading is completed if all the visible services have been loaded successfully", () => { - testSaga(handleFirstVisibleServiceLoadSaga) - .next() - .select(isFirstVisibleServiceLoadCompletedSelector) - .next(false) - .select(visibleServicesDetailLoadStateSelector) - .next(pot.some(undefined)) - .put(firstServiceLoadSuccess()) - .next() - .isDone(); - }); - - it("does nothing if the visible services are not loaded (error)", () => { - testSaga(handleFirstVisibleServiceLoadSaga) - .next() - .select(isFirstVisibleServiceLoadCompletedSelector) - .next(false) - .select(visibleServicesDetailLoadStateSelector) - .next(pot.noneError) - .isDone(); - }); -}); diff --git a/ts/sagas/services/__tests__/handleOrganizationNameUpdateSaga.test.ts b/ts/sagas/services/__tests__/handleOrganizationNameUpdateSaga.test.ts deleted file mode 100644 index 8cc887097e3..00000000000 --- a/ts/sagas/services/__tests__/handleOrganizationNameUpdateSaga.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; -import { testSaga } from "redux-saga-test-plan"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { updateOrganizations } from "../../../store/actions/organizations"; -import { - organizationNamesByFiscalCodeSelector, - OrganizationNamesByFiscalCodeState -} from "../../../store/reducers/entities/organizations/organizationsByFiscalCodeReducer"; -import { visibleServicesSelector } from "../../../store/reducers/entities/services/visibleServices"; -import { mockedService } from "../../../features/services/details/saga/__tests__/handleServiceDetails.test"; -import { handleOrganizationNameUpdateSaga } from "../handleOrganizationNameUpdateSaga"; - -const mockedOrganizationsNameByFiscalCode: OrganizationNamesByFiscalCodeState = - { - ["01"]: "ente1" as NonEmptyString, - ["02"]: "ente2" as NonEmptyString, - ["03"]: "ente3" as NonEmptyString - }; - -const mockedOrganizationsNameByFiscalCodeUpdated: OrganizationNamesByFiscalCodeState = - { - ["01"]: "ente1 - nuovo nome" as NonEmptyString, - ["02"]: "ente2" as NonEmptyString, - ["03"]: "ente3" as NonEmptyString - }; - -const mockedVisibleServices = pot.some([ - { service_id: "A01" as ServiceId, version: 1 }, - { service_id: "A02" as ServiceId, version: 5 }, - { service_id: "A03" as ServiceId, version: 2 } -]); - -describe("handleOrganizationNameUpdateSaga", () => { - it("does nothing if the organizationNamesByFiscalCodeSelector return undefined", () => { - testSaga(handleOrganizationNameUpdateSaga, mockedService) - .next() - .select(organizationNamesByFiscalCodeSelector) - .next(undefined) - .isDone(); - }); - - it("does nothing after service detail load if the related organization name exist in the organizations redux store", () => { - testSaga(handleOrganizationNameUpdateSaga, mockedService) - .next() - .select(organizationNamesByFiscalCodeSelector) - .next(mockedOrganizationsNameByFiscalCodeUpdated) - .select(visibleServicesSelector) - .next(mockedVisibleServices) - .isDone(); - }); - - it("add the organization name of the loaded service detail if the related fiscal code does NOT exist in the organizations redux state", () => { - testSaga(handleOrganizationNameUpdateSaga, mockedService) - .next() - .select(organizationNamesByFiscalCodeSelector) - .next({}) - .put(updateOrganizations(mockedService)) - .next() - .isDone(); - }); - - it("add the organization name of the loaded service detail if the related fiscal code exists in the organizations redux state related to a different name", () => { - testSaga(handleOrganizationNameUpdateSaga, mockedService) - .next() - .select(organizationNamesByFiscalCodeSelector) - .next(mockedOrganizationsNameByFiscalCode) - .select(visibleServicesSelector) - .next(mockedVisibleServices) - .put(updateOrganizations(mockedService)) - .next() - .isDone(); - }); -}); diff --git a/ts/sagas/services/__tests__/handleServiceReadabilitySaga.test.ts b/ts/sagas/services/__tests__/handleServiceReadabilitySaga.test.ts deleted file mode 100644 index 02cbec94577..00000000000 --- a/ts/sagas/services/__tests__/handleServiceReadabilitySaga.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { testSaga } from "redux-saga-test-plan"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { markServiceAsRead } from "../../../store/actions/services"; -import { isFirstVisibleServiceLoadCompletedSelector } from "../../../store/reducers/entities/services/firstServicesLoading"; -import { handleServiceReadabilitySaga } from "../handleServiceReadabilitySaga"; - -describe("handleServiceReadabilitySaga", () => { - const mockedServiceId = "0123" as ServiceId; - - it("makes the service with the given id being marked as read if the first service loading is not complete", () => { - testSaga(handleServiceReadabilitySaga, mockedServiceId) - .next() - .select(isFirstVisibleServiceLoadCompletedSelector) - .next(false) - .put(markServiceAsRead(mockedServiceId)) - .next() - .isDone(); - }); - - it("do nothing if the first service loading has been completed ", () => { - testSaga(handleServiceReadabilitySaga, mockedServiceId) - .next() - .select(isFirstVisibleServiceLoadCompletedSelector) - .next(true) - .isDone(); - }); -}); diff --git a/ts/sagas/services/__tests__/refreshStoredServices.test.ts b/ts/sagas/services/__tests__/refreshStoredServices.test.ts deleted file mode 100644 index 806898fb85d..00000000000 --- a/ts/sagas/services/__tests__/refreshStoredServices.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; -import { testSaga } from "redux-saga-test-plan"; -import { DepartmentName } from "../../../../definitions/backend/DepartmentName"; -import { OrganizationName } from "../../../../definitions/backend/OrganizationName"; -import { PaginatedServiceTupleCollection } from "../../../../definitions/backend/PaginatedServiceTupleCollection"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { ServiceName } from "../../../../definitions/backend/ServiceName"; -import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; -import { loadServicesDetail } from "../../../store/actions/services"; -import { - servicesByIdSelector, - ServicesByIdState -} from "../../../features/services/details/store/reducers/servicesById"; -import { refreshStoredServices } from "../refreshStoredServices"; - -describe("refreshStoredServices", () => { - const mockedService: ServicePublic = { - service_id: "S01" as ServiceId, - service_name: "servizio1" as ServiceName, - organization_name: "ente1" as OrganizationName, - department_name: "dipartimento1" as DepartmentName, - organization_fiscal_code: "01" as OrganizationFiscalCode, - version: 1 - }; - - it("loads again the services if it is visible and an old version is stored", () => { - const mockedNewVisibleServices: PaginatedServiceTupleCollection["items"] = [ - { service_id: mockedService.service_id, version: 2 } - ]; - const mockedServicesById: ServicesByIdState = { - [mockedService.service_id]: pot.some(mockedService) - }; - - testSaga(refreshStoredServices, mockedNewVisibleServices) - .next() - .select(servicesByIdSelector) - .next(mockedServicesById) - .put(loadServicesDetail([mockedService.service_id])) - .next() - .isDone(); - }); - - it("loads the services if it is visible and not yet stored", () => { - const mockedNewVisibleServices: PaginatedServiceTupleCollection["items"] = [ - { service_id: mockedService.service_id, version: 1 } - ]; - - testSaga(refreshStoredServices, mockedNewVisibleServices) - .next() - .select(servicesByIdSelector) - .next({}) - .put(loadServicesDetail([mockedService.service_id])) - .next() - .isDone(); - }); - - it("does nothing if all the latest versions of visible services are stored", () => { - const mockedVisibleServices: PaginatedServiceTupleCollection["items"] = [ - { service_id: mockedService.service_id, version: mockedService.version } - ]; - const mockedServicesById: ServicesByIdState = { - [mockedService.service_id]: pot.some(mockedService) - }; - - testSaga(refreshStoredServices, mockedVisibleServices) - .next() - .select(servicesByIdSelector) - .next(mockedServicesById) - .next() - .isDone(); - }); -}); diff --git a/ts/sagas/services/handleFirstVisibleServiceLoadSaga.ts b/ts/sagas/services/handleFirstVisibleServiceLoadSaga.ts deleted file mode 100644 index 14fc7a0a287..00000000000 --- a/ts/sagas/services/handleFirstVisibleServiceLoadSaga.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { SagaIterator } from "redux-saga"; -import { put, select } from "typed-redux-saga/macro"; -import { firstServiceLoadSuccess } from "../../store/actions/services"; -import { visibleServicesDetailLoadStateSelector } from "../../store/reducers/entities/services"; -import { isFirstVisibleServiceLoadCompletedSelector } from "../../store/reducers/entities/services/firstServicesLoading"; - -/** - * A function to check if all services detail and scopes are loaded with success. - * If it is true, by dispatching firstServiceLoadSuccess the app stop considering loaded services as read - */ -export function* handleFirstVisibleServiceLoadSaga(): SagaIterator { - const isFirstVisibleServiceLoadCompleted: ReturnType< - typeof isFirstVisibleServiceLoadCompletedSelector - > = yield* select(isFirstVisibleServiceLoadCompletedSelector); - if (!isFirstVisibleServiceLoadCompleted) { - const visibleServicesDetailsLoadState: ReturnType< - typeof visibleServicesDetailLoadStateSelector - > = yield* select(visibleServicesDetailLoadStateSelector); - if (pot.isSome(visibleServicesDetailsLoadState)) { - yield* put(firstServiceLoadSuccess()); - } - } -} diff --git a/ts/sagas/services/handleOrganizationNameUpdateSaga.ts b/ts/sagas/services/handleOrganizationNameUpdateSaga.ts deleted file mode 100644 index 76f056f9d79..00000000000 --- a/ts/sagas/services/handleOrganizationNameUpdateSaga.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { SagaIterator } from "redux-saga"; -import { put, select } from "typed-redux-saga/macro"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import { updateOrganizations } from "../../store/actions/organizations"; -import { - organizationNamesByFiscalCodeSelector, - OrganizationNamesByFiscalCodeState -} from "../../store/reducers/entities/organizations/organizationsByFiscalCodeReducer"; -import { - visibleServicesSelector, - VisibleServicesState -} from "../../store/reducers/entities/services/visibleServices"; -import { isVisibleService } from "../../utils/services"; - -/** - * A function to check if the organization_name included in the service detail is different - * from the stored organization name (for the same organization_fiscal_code). - * If true, the incoming one is saved into the redux store. - * @param service - */ -export function* handleOrganizationNameUpdateSaga( - service: ServicePublic -): SagaIterator { - // If the organization fiscal code is associated to different organization names, - // it is considered valid the one declared for a visible service - const organizations: OrganizationNamesByFiscalCodeState = yield* select( - organizationNamesByFiscalCodeSelector - ); - if (organizations) { - const fc = service.organization_fiscal_code; - - // The organization is stored if the corresponding fiscal code has no maches among those stored - const organization = organizations[fc]; - if (!organization) { - yield* put(updateOrganizations(service)); - return; - } - const visibleServices: VisibleServicesState = yield* select( - visibleServicesSelector - ); - const isVisible = - isVisibleService(visibleServices, pot.some(service)) || false; - - // If the organization has been previously saved in the organization entity, - // the organization name is updated only if the related service is visible - // and the name has been updated - if (isVisible && organization !== service.organization_name) { - yield* put(updateOrganizations(service)); - } - } -} diff --git a/ts/sagas/services/handleServiceReadabilitySaga.ts b/ts/sagas/services/handleServiceReadabilitySaga.ts deleted file mode 100644 index 468dae30e6c..00000000000 --- a/ts/sagas/services/handleServiceReadabilitySaga.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SagaIterator } from "redux-saga"; -import { put, select } from "typed-redux-saga/macro"; -import { ServiceId } from "../../../definitions/backend/ServiceId"; -import { markServiceAsRead } from "../../store/actions/services"; -import { isFirstVisibleServiceLoadCompletedSelector } from "../../store/reducers/entities/services/firstServicesLoading"; - -/** - * A function to check if a service is loaded for the first time (at first startup or when the cache - * is cleaned). If true, the app shows the service list item without badge - * @param serviceId - */ -export function* handleServiceReadabilitySaga(serviceId: string): SagaIterator { - const isFirstVisibleServiceLoadCompleted: ReturnType< - typeof isFirstVisibleServiceLoadCompletedSelector - > = yield* select(isFirstVisibleServiceLoadCompletedSelector); - - if (!isFirstVisibleServiceLoadCompleted) { - yield* put(markServiceAsRead(serviceId as ServiceId)); - } -} diff --git a/ts/sagas/services/refreshStoredServices.ts b/ts/sagas/services/refreshStoredServices.ts deleted file mode 100644 index a5363fefbe9..00000000000 --- a/ts/sagas/services/refreshStoredServices.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { SagaIterator } from "redux-saga"; -import { put, select } from "typed-redux-saga/macro"; -import { PaginatedServiceTupleCollection } from "../../../definitions/backend/PaginatedServiceTupleCollection"; -import { loadServicesDetail } from "../../store/actions/services"; -import { servicesByIdSelector } from "../../features/services/details/store/reducers/servicesById"; - -/** - * Check which services detail must be loaded. If there are, loading action will be dispatched - * A service detail has to be loaded if one of these conditions is true - * - it is not into the store (servicesByIdSelector) - * - the relative stored value is a pot none (but not loading) - * - the relative stored value is some and its version is less than the visible one - * @param visibleServices - */ -export function* refreshStoredServices( - visibleServices: PaginatedServiceTupleCollection["items"] -): SagaIterator { - const storedServicesById: ReturnType = - yield* select(servicesByIdSelector); - - const serviceDetailIdsToLoad = visibleServices - .filter(service => { - const serviceId = service.service_id; - const storedService = storedServicesById[serviceId]; - return ( - // The service detail: - // - is not in the redux store - storedService === undefined || - // retry to load those services that are in error state - pot.isError(storedService) || - // - is in the redux store as PotNone and not loading - pot.isNone(storedService) || - // - is in the redux store as PotSome, is not updating and is outdated - (pot.isSome(storedService) && - !pot.isUpdating(storedService) && - storedService.value.version < service.version) - ); - }) - .map(_ => _.service_id); - if (serviceDetailIdsToLoad.length > 0) { - yield* put(loadServicesDetail(serviceDetailIdsToLoad)); - } -} diff --git a/ts/sagas/services/removeUnusedStoredServices.ts b/ts/sagas/services/removeUnusedStoredServices.ts deleted file mode 100644 index bbc5d81d7a2..00000000000 --- a/ts/sagas/services/removeUnusedStoredServices.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { ITuple2, Tuple2 } from "@pagopa/ts-commons/lib/tuples"; -import { SagaIterator } from "redux-saga"; -import { put, select } from "typed-redux-saga/macro"; -import { PaginatedServiceTupleCollection } from "../../../definitions/backend/PaginatedServiceTupleCollection"; -import { removeServiceTuples } from "../../store/actions/services"; -import { servicesByIdSelector } from "../../features/services/details/store/reducers/servicesById"; - -type VisibleServiceVersionById = { - [index: string]: number | undefined; -}; - -export function* removeUnusedStoredServices( - visibleServices: PaginatedServiceTupleCollection["items"] -): SagaIterator { - const visibleServiceVersionById = - visibleServices.reduce( - (accumulator, currentValue) => ({ - ...accumulator, - [currentValue.service_id]: currentValue.version - }), - {} - ); - - const storedServicesById: ReturnType = - yield* select(servicesByIdSelector); - - // Create an array of tuples containing: - // - serviceId (to remove service from both the servicesById and the servicesMetadataById sections of the redux store) - // - organizationFiscalCode (to remove service from serviceIdsByOrganizationFiscalCode - // section of the redux store) - const serviceTuplesToRemove = Object.keys(storedServicesById).reduce< - ReadonlyArray> - >((accumulator, serviceId) => { - // Check if this service id must be removed - // A service must be removed if is no more visible and not used by any loaded message. - const mustRemoveServiceId = - visibleServiceVersionById[serviceId] === undefined; - - if (!mustRemoveServiceId) { - return accumulator; - } - - const storedPotService = storedServicesById[serviceId]; - - if (storedPotService !== undefined) { - // If the service detail is also loaded get the organization fiscal code - const organizationFiscalCode = pot.toUndefined( - pot.map(storedPotService, _ => _.organization_fiscal_code) - ); - - return [...accumulator, Tuple2(serviceId, organizationFiscalCode)]; - } - - return accumulator; - }, []); - - // Dispatch action to remove the services from the redux store - yield* put(removeServiceTuples(serviceTuplesToRemove)); -} diff --git a/ts/sagas/services/watchLoadServicesSaga.ts b/ts/sagas/services/watchLoadServicesSaga.ts deleted file mode 100644 index a084c99caad..00000000000 --- a/ts/sagas/services/watchLoadServicesSaga.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { SagaIterator } from "redux-saga"; -import { fork, put, takeEvery } from "typed-redux-saga/macro"; -import { getType } from "typesafe-actions"; -import { BackendClient } from "../../api/backend"; -import { loadServiceDetail } from "../../features/services/details/store/actions/details"; -import { loadVisibleServices } from "../../store/actions/services"; -import { watchServicesDetailLoadSaga } from "../startup/loadServiceDetailRequestHandler"; -import { loadVisibleServicesRequestHandler } from "../startup/loadVisibleServicesHandler"; -import { watchServicesDetailsSaga } from "../../features/services/details/saga"; -import { handleFirstVisibleServiceLoadSaga } from "./handleFirstVisibleServiceLoadSaga"; - -/** - * A saga for managing requests to load/refresh services data from backend - * @param backendClient - */ -export function* watchLoadServicesSaga( - backendClient: ReturnType -): SagaIterator { - yield* takeEvery( - getType(loadVisibleServices.request), - loadVisibleServicesRequestHandler, - backendClient.getVisibleServices - ); - - yield* fork(watchServicesDetailsSaga, backendClient); - - // start a watcher to handle the load of services details in a bunch (i.e when visible services are loaded) - yield* fork(watchServicesDetailLoadSaga, backendClient.getService); - - // TODO: it could be implemented in a forked saga being canceled as soon as - // isFirstServiceLoadCOmpleted is true (https://redux-saga.js.org/docs/advanced/TaskCancellation.html) - yield* takeEvery( - getType(loadServiceDetail.success), - handleFirstVisibleServiceLoadSaga - ); - - // Load/refresh services content - yield* put(loadVisibleServices.request()); -} diff --git a/ts/sagas/startup/__tests__/loadVisibleServicesHandler.test.ts b/ts/sagas/startup/__tests__/loadVisibleServicesHandler.test.ts deleted file mode 100644 index 0d659f2f3bf..00000000000 --- a/ts/sagas/startup/__tests__/loadVisibleServicesHandler.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as E from "fp-ts/lib/Either"; -import { testSaga } from "redux-saga-test-plan"; -import { PaginatedServiceTupleCollection } from "../../../../definitions/backend/PaginatedServiceTupleCollection"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { loadVisibleServices } from "../../../store/actions/services"; -import { loadVisibleServicesRequestHandler } from "../loadVisibleServicesHandler"; -import { withRefreshApiCall } from "../../../features/fastLogin/saga/utils"; - -describe("loadVisibleServicesHandler", () => { - describe("loadVisibleServicesRequestHandler", () => { - const getVisibleServices = jest.fn(); - - const mockedVisibleServices: PaginatedServiceTupleCollection["items"] = [ - { service_id: "A01" as ServiceId, version: 1 }, - { service_id: "A02" as ServiceId, version: 5 }, - { service_id: "A03" as ServiceId, version: 2 } - ]; - - it("returns a generic error if backend response is 500", () => { - testSaga(loadVisibleServicesRequestHandler, getVisibleServices) - .next() - .call(withRefreshApiCall, getVisibleServices({}), { - skipThrowingError: true - }) - .next( - E.right({ - status: 500, - value: "An error occurred loading visible services" - }) - ) - .put( - loadVisibleServices.failure( - new Error("An error occurred loading visible services") - ) - ) - .next() - .isDone(); - }); - - describe("returns an error even if the backend response is 401", () => { - it("the session expiration is handled by withRefreshApiCall", () => { - testSaga(loadVisibleServicesRequestHandler, getVisibleServices) - .next() - .call(withRefreshApiCall, getVisibleServices({}), { - skipThrowingError: true - }) - .next( - E.right({ - status: 403, - value: "An error occurred loading visible services" - }) - ) - .put( - loadVisibleServices.failure( - new Error("An error occurred loading visible services") - ) - ) - .next() - .isDone(); - }); - }); - - it("return an array of visibile services if backend response is 200", () => { - testSaga(loadVisibleServicesRequestHandler, getVisibleServices) - .next() - .call(withRefreshApiCall, getVisibleServices({}), { - skipThrowingError: true - }) - .next(E.right({ status: 200, value: { items: mockedVisibleServices } })) - .put(loadVisibleServices.success(mockedVisibleServices)) - .next(); - }); - }); -}); diff --git a/ts/sagas/startup/loadServiceDetailRequestHandler.ts b/ts/sagas/startup/loadServiceDetailRequestHandler.ts deleted file mode 100644 index 023036e5825..00000000000 --- a/ts/sagas/startup/loadServiceDetailRequestHandler.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Millisecond } from "@pagopa/ts-commons/lib/units"; -import { buffers, channel, Channel } from "redux-saga"; -import { call, fork, take, takeLatest } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; -import { BackendClient } from "../../api/backend"; -import { totServiceFetchWorkers } from "../../config"; -import { applicationChangeState } from "../../store/actions/application"; -import { loadServiceDetail } from "../../features/services/details/store/actions/details"; -import { loadServicesDetail } from "../../store/actions/services"; -import { trackServiceDetailLoadingStatistics } from "../../utils/analytics"; -import { handleServiceDetails } from "../../features/services/details/saga/handleServiceDetails"; - -/** - * A generator that listen for loadServiceDetail.request from a channel and perform the - * handling. - * - * @param requestsChannel The channel where to take the loadServiceDetail.request actions - * @param getService API call to fetch the service detail - */ -function* handleServiceLoadRequest( - requestsChannel: Channel>, - getService: ReturnType["getService"] -) { - // Infinite loop that wait and process loadServiceDetail requests from the channel - while (true) { - const action: ActionType = yield* take( - requestsChannel - ); - yield* call(handleServiceDetails, getService, action); - } -} - -/** - * create an event channel to buffer all services detail loading requests - * it watches for loadServicesDetail (multiple services id) and for each of them, it puts a - * loadServiceDetail.request event into that channel - * The workers (the handlers) will consume the channel data - * @param getService - */ -export function* watchServicesDetailLoadSaga( - getService: ReturnType["getService"] -) { - // start a saga to track services detail load stats - yield* fork(watchLoadServicesDetailToTrack); - - // Create the channel used for the communication with the handlers. - const requestsChannel = (yield* call( - channel, - buffers.expanding() - )) as Channel>; - - // fork the handlers - // eslint-disable-next-line - for (let i = 0; i < totServiceFetchWorkers; i++) { - yield* fork(handleServiceLoadRequest, requestsChannel, getService); - } - - while (true) { - // Take the loadServicesDetail action and for each service id - // put back a loadServiceDetail.request in the channel - // to be processed by the handlers. - const action = yield* take(loadServicesDetail); - - action.payload.forEach((serviceId: string) => - requestsChannel.put(loadServiceDetail.request(serviceId)) - ); - } -} - -const calculateLoadingTime = (startTime: Millisecond): Millisecond => - (startTime !== 0 ? new Date().getTime() - startTime : 0) as Millisecond; - -/** - * listen for loading services details events to extract some track information - * like amount of details to load and how much time they take - */ -function* watchLoadServicesDetailToTrack() { - yield* takeLatest( - [loadServicesDetail, loadServiceDetail.success, applicationChangeState], - action => { - switch (action.type) { - // request to load a set of services detail - // copying object is needed to avoid "immutable" error on frozen objects - case getType(loadServicesDetail): - const stats: ServicesDetailLoadTrack = { - ...servicesDetailLoadTrack, - kind: undefined, - startTime: new Date().getTime() as Millisecond, - servicesId: new Set([...action.payload]), - loaded: 0, - toLoad: action.payload.length - }; - servicesDetailLoadTrack = stats; - break; - // single service detail is been loaded - case getType(loadServiceDetail.success): - servicesDetailLoadTrack.servicesId.delete(action.payload.service_id); - const statsServiceLoad: ServicesDetailLoadTrack = { - ...servicesDetailLoadTrack, - loaded: - servicesDetailLoadTrack.toLoad - - servicesDetailLoadTrack.servicesId.size - }; - servicesDetailLoadTrack = statsServiceLoad; - if ( - statsServiceLoad.servicesId.size === 0 && - statsServiceLoad.loaded > 0 - ) { - // all service are been loaded - trackServicesDetailLoad({ - ...servicesDetailLoadTrack, - kind: "COMPLETE", - loadingTime: calculateLoadingTime( - servicesDetailLoadTrack.startTime - ) - }); - } - - break; - // app changes state - case getType(applicationChangeState): - /** - * if the app went in inactive or background state these measurements - * could be not valid since the OS could apply a freeze or a limitation around the app context - * so the app could run but with few limitations - */ - if ( - action.payload !== "active" && - servicesDetailLoadTrack.servicesId.size > 0 - ) { - trackServicesDetailLoad({ - ...servicesDetailLoadTrack, - loadingTime: calculateLoadingTime( - servicesDetailLoadTrack.startTime - ), - kind: "PARTIAL" - }); - } - // app comes back active, restore stats - else if (action.payload === "active") { - servicesDetailLoadTrack = { - ...servicesDetailLoadTrack, - kind: undefined, - startTime: new Date().getTime() as Millisecond, - loaded: 0, - toLoad: servicesDetailLoadTrack.servicesId.size - }; - } - break; - } - } - ); -} - -export type ServicesDetailLoadTrack = { - // when loading starts - startTime: Millisecond; - // the amount of loading millis - loadingTime: Millisecond; - // the amount of services detail to load - toLoad: number; - // the amount of services detail loaded - loaded: number; - // the set of the services id that remain to be loaded - servicesId: Set; - // COMPLETE: all services detail are been loaded - // PARTIAL: a sub-set of services detail to load are been loaded - kind?: "COMPLETE" | "PARTIAL"; -}; - -const defaultDetailLoadTrack = (): ServicesDetailLoadTrack => ({ - startTime: 0 as Millisecond, - loadingTime: 0 as Millisecond, - toLoad: 0, - loaded: 0, - servicesId: new Set() -}); - -// eslint-disable-next-line functional/no-let -let servicesDetailLoadTrack = defaultDetailLoadTrack(); - -const trackServicesDetailLoad = (trackingStats: ServicesDetailLoadTrack) => { - trackServiceDetailLoadingStatistics(trackingStats); - // reset on complete - // when it is "PARTIAL" data must be keep to be used when the app come active again - if (trackingStats.kind === "COMPLETE") { - // reset data - servicesDetailLoadTrack = defaultDetailLoadTrack(); - } -}; diff --git a/ts/sagas/startup/loadVisibleServicesHandler.ts b/ts/sagas/startup/loadVisibleServicesHandler.ts deleted file mode 100644 index 8826c0a0527..00000000000 --- a/ts/sagas/startup/loadVisibleServicesHandler.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { readableReport } from "@pagopa/ts-commons/lib/reporters"; -import * as E from "fp-ts/lib/Either"; -import { call, put, select } from "typed-redux-saga/macro"; -import { BackendClient } from "../../api/backend"; -import { loadVisibleServices } from "../../store/actions/services"; -import { ReduxSagaEffect, SagaCallReturnType } from "../../types/utils"; -import { convertUnknownToError } from "../../utils/errors"; -import { refreshStoredServices } from "../services/refreshStoredServices"; -import { removeUnusedStoredServices } from "../services/removeUnusedStoredServices"; -import { withRefreshApiCall } from "../../features/fastLogin/saga/utils"; -import { isFastLoginEnabledSelector } from "../../features/fastLogin/store/selectors"; - -/** - * A generator to load the service details from the Backend - * - * @param {function} getService - The function that makes the Backend request - * @param {string} id - The id of the service to load - * @returns {IterableIterator>} - */ -export function* loadVisibleServicesRequestHandler( - getVisibleServices: ReturnType["getVisibleServices"] -): Generator< - ReduxSagaEffect, - void, - SagaCallReturnType -> { - try { - const response = (yield* call(withRefreshApiCall, getVisibleServices({}), { - skipThrowingError: true - })) as unknown as SagaCallReturnType; - - if (E.isLeft(response)) { - throw Error(readableReport(response.left)); - } - if (response.right.status === 401) { - const isFastLoginEnabled = yield* select(isFastLoginEnabledSelector); - if (isFastLoginEnabled) { - return; - } - } - if (response.right.status === 200) { - const { items: visibleServices } = response.right.value; - yield* put(loadVisibleServices.success(visibleServices)); - - // Check if old version of services are stored and load new available versions of services - yield* call(removeUnusedStoredServices, visibleServices); - yield* call(refreshStoredServices, visibleServices); - } else { - throw Error("An error occurred loading visible services"); - } - } catch (e) { - yield* put(loadVisibleServices.failure(convertUnknownToError(e))); - } -} diff --git a/ts/screens/services/LegacyServiceDetailsScreen.tsx b/ts/screens/services/LegacyServiceDetailsScreen.tsx deleted file mode 100644 index 8edfdc1bac4..00000000000 --- a/ts/screens/services/LegacyServiceDetailsScreen.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import * as React from "react"; -import { useEffect, useState } from "react"; -import { View, SafeAreaView, ScrollView } from "react-native"; -import { connect } from "react-redux"; -import { ContentWrapper, VSpacer } from "@pagopa/io-app-design-system"; -import { Route, useRoute } from "@react-navigation/native"; -import { ServiceId } from "../../../definitions/backend/ServiceId"; -import { SpecialServiceMetadata } from "../../../definitions/backend/SpecialServiceMetadata"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import ExtractedCTABar from "../../components/cta/ExtractedCTABar"; -import OrganizationHeader from "../../components/OrganizationHeader"; -import BaseScreenComponent, { - ContextualHelpPropsMarkdown -} from "../../components/screens/BaseScreenComponent"; -import { EdgeBorderComponent } from "../../components/screens/EdgeBorderComponent"; -import ContactPreferencesToggles from "../../components/services/ContactPreferencesToggles"; -import ServiceMetadataComponent from "../../components/services/ServiceMetadata"; -import LegacySpecialServicesCTA from "../../components/services/SpecialServices/LegacySpecialServicesCTA"; -import TosAndPrivacyBox from "../../components/services/TosAndPrivacyBox"; -import LegacyMarkdown from "../../components/ui/Markdown/LegacyMarkdown"; -import { FooterTopShadow } from "../../components/FooterTopShadow"; -import I18n from "../../i18n"; -import { useIONavigation } from "../../navigation/params/AppParamsList"; -import { loadServiceDetail } from "../../features/services/details/store/actions/details"; -import { Dispatch } from "../../store/actions/types"; -import { contentSelector } from "../../store/reducers/content"; -import { isDebugModeEnabledSelector } from "../../store/reducers/debug"; -import { serviceByIdPotSelector } from "../../features/services/details/store/reducers/servicesById"; -import { - isEmailEnabledSelector, - isInboxEnabledSelector, - isProfileEmailValidatedSelector, - profileSelector -} from "../../store/reducers/profile"; -import { GlobalState } from "../../store/reducers/types"; -import { getServiceCTA } from "../../features/messages/utils/messages"; -import { logosForService } from "../../utils/services"; -import { handleItemOnPress } from "../../utils/url"; -import { useIOSelector } from "../../store/hooks"; -import { ServiceDetailsScreenRouteParams } from "../../features/services/details/screens/ServiceDetailsScreen"; - -type Props = ReturnType & - ReturnType; - -type SpecialServiceWrapper = { - isSpecialService: boolean; - customSpecialFlowOpt?: string; -}; - -const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { - title: "serviceDetail.headerTitle", - body: "serviceDetail.contextualHelpContent" -}; - -/** - * Screen displaying the details of a selected service. The user - * can enable/disable the service and customize the notification settings. - */ -const LegacyServiceDetailsScreen = (props: Props) => { - const [isMarkdownLoaded, setIsMarkdownLoaded] = useState(false); - const navigation = useIONavigation(); - const { serviceId, activate } = - useRoute>().params; - - const service = useIOSelector(state => - pipe(serviceByIdPotSelector(state, serviceId), pot.toUndefined) - ); - - // const serviceId = props.route.params.serviceId; - // const activate = props.route.params.activate; - - const { loadServiceDetail } = props; - useEffect(() => { - loadServiceDetail(serviceId); - }, [serviceId, loadServiceDetail]); - - const onMarkdownEnd = () => setIsMarkdownLoaded(true); - - // This has been considered just to avoid compiling errors - // once we navigate from list or a message we always have the service data since they're previously loaded - if (service === undefined) { - return null; - } - - const metadata = service.service_metadata; - - // if markdown content is not available, render immediately what is possible - // but we must wait for metadata load to be completed to avoid flashes - const isMarkdownAvailable = metadata?.description; - // if markdown data is available, wait for it to be rendered - const canRenderItems = isMarkdownAvailable ? isMarkdownLoaded : true; - - const specialServiceInfoOpt = SpecialServiceMetadata.is(metadata) - ? ({ - isSpecialService: true, - customSpecialFlowOpt: metadata.custom_special_flow - } as SpecialServiceWrapper) - : undefined; - - const maybeCTA = getServiceCTA(metadata); - const showCTA = - (O.isSome(maybeCTA) || !!specialServiceInfoOpt) && canRenderItems; - - return ( - navigation.goBack()} - headerTitle={I18n.t("serviceDetail.headerTitle")} - contextualHelpMarkdown={contextualHelpMarkdown} - faqCategories={["services_detail"]} - > - - - - - - - {metadata?.description && ( - <> - - {metadata.description} - - - - )} - - {canRenderItems && ( - <> - {metadata && ( - <> - - - - )} - - - - - - - - - )} - - - - {showCTA && ( - - {O.isSome(maybeCTA) && ( - - - - )} - {!!specialServiceInfoOpt && ( - <> - - - - )} - - )} - - - ); -}; - -const mapStateToProps = (state: GlobalState) => ({ - isInboxEnabled: isInboxEnabledSelector(state), - isEmailEnabled: isEmailEnabledSelector(state), - isEmailValidated: isProfileEmailValidatedSelector(state), - content: contentSelector(state), - profile: profileSelector(state), - isDebugModeEnabled: isDebugModeEnabledSelector(state) -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - loadServiceDetail: (id: ServiceId) => dispatch(loadServiceDetail.request(id)), - dispatch -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(LegacyServiceDetailsScreen); diff --git a/ts/screens/services/ServicesHomeScreen.tsx b/ts/screens/services/ServicesHomeScreen.tsx deleted file mode 100644 index 0bc9d6552e3..00000000000 --- a/ts/screens/services/ServicesHomeScreen.tsx +++ /dev/null @@ -1,541 +0,0 @@ -/** - * A screen that contains all the available servives. - * - Local tab: services sections related to the areas of interest selected by the user - * - National tab: national services sections - * - All: local and national services sections, not including the user areas of interest - * - * A 'loading component' is displayed (hiding the tabs content) if: - * - visible servcices are loading, or - * - userMetadata is loading, or - * - servicesByScope is loading - * - * An 'error component' is displayed (hiding the tabs content) if: - * - userMetadata load fails, or - * - visible services load fails - * - * A loader on tabs is displayed (not hiding the tabs content) if: - * - userMetadata is updating, or - * - visible services are refreshed - * - * If toastContent is undefined, when userMetadata/visible services are loading/error, - * tabs are hidden and they are displayed renderServiceLoadingPlaceholder/renderErrorPlaceholder - * - */ -import { IOColors, IOToast, VSpacer } from "@pagopa/io-app-design-system"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import * as React from "react"; -import { - Image, - KeyboardAvoidingView, - Platform, - StyleSheet, - View -} from "react-native"; -import { connect } from "react-redux"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import SectionStatusComponent from "../../components/SectionStatus"; -import { Body } from "../../components/core/typography/Body"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; -import GenericErrorComponent from "../../components/screens/GenericErrorComponent"; -import TopScreenComponent from "../../components/screens/TopScreenComponent"; -import { MIN_CHARACTER_SEARCH_TEXT } from "../../components/search/SearchButton"; -import { SearchNoResultMessage } from "../../components/search/SearchNoResultMessage"; -import ServicesSearch from "../../components/services/ServicesSearch"; -import FocusAwareStatusBar from "../../components/ui/FocusAwareStatusBar"; -import I18n from "../../i18n"; -import ServicesHomeTabNavigator from "../../navigation/ServicesHomeTabNavigator"; -import { - navigateToServiceDetailsScreen, - navigateToServicePreferenceScreen -} from "../../store/actions/navigation"; -import { profileUpsert } from "../../store/actions/profile"; -import { - loadVisibleServices, - showServiceDetails -} from "../../store/actions/services"; -import { Dispatch } from "../../store/actions/types"; -import { - userMetadataLoad, - userMetadataUpsert -} from "../../store/actions/userMetadata"; -import { - ServicesSectionState, - nationalServicesSectionsSelector, - notSelectedServicesSectionsSelector, - selectedLocalServicesSectionsSelector, - servicesSelector, - visibleServicesDetailLoadStateSelector -} from "../../store/reducers/entities/services"; -import { readServicesByIdSelector } from "../../store/reducers/entities/services/readStateByServiceId"; -import { servicesByIdSelector } from "../../features/services/details/store/reducers/servicesById"; -import { visibleServicesSelector } from "../../store/reducers/entities/services/visibleServices"; -import { wasServiceAlertDisplayedOnceSelector } from "../../store/reducers/persistedPreferences"; -import { ProfileState, profileSelector } from "../../store/reducers/profile"; -import { - isSearchServicesEnabledSelector, - searchTextSelector -} from "../../store/reducers/search"; -import { GlobalState } from "../../store/reducers/types"; -import { - UserMetadata, - userMetadataSelector -} from "../../store/reducers/userMetadata"; -import customVariables from "../../theme/variables"; -import { - getChannelsforServicesList, - getProfileChannelsforServicesList -} from "../../utils/profile"; -import { ServiceDetailsScreenRouteParams } from "../../features/services/details/screens/ServiceDetailsScreen"; - -type ReduxMergedProps = Readonly<{ - updateOrganizationsOfInterestMetadata: ( - selectedItemIds: O.Option> - ) => void; -}>; - -type Props = ReturnType & - ReturnType & - ReduxMergedProps; - -type State = { - currentTab: number; - currentTabServicesId: ReadonlyArray; - isLongPressEnabled: boolean; - enableServices: boolean; - toastErrorMessage: string; - isInnerContentRendered: boolean; -}; - -type DataLoadFailure = - | "servicesLoadFailure" - | "userMetadaLoadFailure" - | undefined; - -const customSpacerHeight = 64; - -const styles = StyleSheet.create({ - container: { - flex: 1 - }, - topScreenContainer: { - flex: 1, - justifyContent: "flex-end" - }, - center: { - alignItems: "center" - }, - padded: { - paddingHorizontal: customVariables.contentPadding - }, - customSpacer: { - height: customSpacerHeight - } -}); - -const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { - title: "services.contextualHelpTitle", - body: "services.contextualHelpContent" -}; - -class ServicesHomeScreen extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - currentTab: 0, - currentTabServicesId: [], - isLongPressEnabled: false, - enableServices: false, - toastErrorMessage: I18n.t("global.genericError"), - isInnerContentRendered: false - }; - } - - /** - * return true if all services have INBOX channel disabled - */ - private areAllServicesInboxChannelDisabled = (): boolean => { - const currentTabServicesChannels = this.props.getServicesChannels( - this.props.tabsServicesId[this.state.currentTab], - this.props.profile - ); - - const disabledServices: number = Object.keys( - currentTabServicesChannels - ).filter( - id => currentTabServicesChannels[id].indexOf("INBOX") !== -1 - ).length; - - return ( - disabledServices > 0 && - disabledServices === Object.keys(currentTabServicesChannels).length - ); - }; - - /** - * if we are displaying the loading screen and we got no errors on loading - * data, then we can show the content - */ - private canRenderContent = () => { - if ( - !this.state.isInnerContentRendered && - pot.isSome(this.props.visibleServicesContentLoadState) && - this.props.loadDataFailure === undefined - ) { - this.setState({ isInnerContentRendered: true }); - } - }; - - public componentDidMount() { - // On mount, update visible services and user metadata if their - // refresh fails - if (pot.isError(this.props.potUserMetadata)) { - this.props.refreshUserMetadata(); - } - - this.canRenderContent(); - - if (pot.isError(this.props.visibleServicesContentLoadState)) { - this.props.refreshVisibleServices(); - } - } - - // TODO: evaluate if it can be replaced by the component introduced within https://www.pivotaltracker.com/story/show/168247501 - private renderServiceLoadingPlaceholder() { - return ( - - {Platform.OS === "ios" && } - - - - - {I18n.t("services.loading.title")} - {I18n.t("services.loading.subtitle")} - - ); - } - - // eslint-disable-next-line - public componentDidUpdate(prevProps: Props, prevState: State) { - // if some errors occur while updating profile, we will show a message in a toast - // profile could be updated by enabling/disabling on or more channel of a service - if ( - pot.isError(this.props.profile) && - !pot.isError(prevProps.profile) && - this.props.profile.error.type !== "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR" - ) { - IOToast.error(I18n.t("serviceDetail.onUpdateEnabledChannelsFailure")); - } - - const enableServices = this.areAllServicesInboxChannelDisabled(); - if (enableServices !== prevState.enableServices) { - this.setState({ enableServices }); - } - - this.canRenderContent(); - - if (this.state.isInnerContentRendered) { - if ( - pot.isError(this.props.potUserMetadata) && - (pot.isUpdating(prevProps.potUserMetadata) || - pot.isLoading(prevProps.potUserMetadata)) - ) { - // A toast is displayed if upsert userMetadata load fails - IOToast.error(this.state.toastErrorMessage); - } - - if ( - pot.isLoading(prevProps.visibleServicesContentLoadState) && - pot.isError(this.props.visibleServicesContentLoadState) - ) { - // A toast is displayed if refresh visible services fails (on content or metadata load) - IOToast.error(this.state.toastErrorMessage); - } - } - } - - private onServiceSelect = (service: ServicePublic) => { - // when a service gets selected the service is recorded as read - this.props.serviceDetailsLoad(service); - this.props.navigateToServiceDetailsScreen({ - serviceId: service.service_id - }); - }; - - private renderErrorContent = () => { - if (this.state.isInnerContentRendered) { - return undefined; - } - - switch (this.props.loadDataFailure) { - case "userMetadaLoadFailure": - return ( - this.refreshScreenContent(true)} - /> - ); - case "servicesLoadFailure": - return ; - default: - return undefined; - } - }; - - private renderInnerContent = () => { - if (this.state.isInnerContentRendered) { - return this.renderTabs(); - } else { - return this.renderServiceLoadingPlaceholder(); - } - }; - - public render() { - return ( - - - - - {this.renderErrorContent() ? ( - this.renderErrorContent() - ) : this.props.isSearchEnabled ? ( - this.renderSearch() - ) : ( - - {/* */} - {this.renderInnerContent()} - - )} - - - - - ); - } - - /** - * Render ServicesSearch component. - */ - private renderSearch = () => - pipe( - this.props.searchText, - O.map(_ => - _.length < MIN_CHARACTER_SEARCH_TEXT ? ( - - ) : ( - - ) - ), - O.getOrElse(() => ( - - )) - ); - - private refreshServices = () => { - this.setState({ - toastErrorMessage: I18n.t("global.genericError") - }); - this.props.refreshVisibleServices(); - }; - - private refreshScreenContent = (hideToast: boolean = false) => { - if (!hideToast) { - this.setState({ toastErrorMessage: I18n.t("global.genericError") }); - } - this.props.refreshUserMetadata(); - this.props.refreshVisibleServices(); - }; - - /** - * Render Locals, Nationals and Other services tabs. - */ - private renderTabs = () => ( - - - - ); -} - -const mapStateToProps = (state: GlobalState) => { - const potUserMetadata = userMetadataSelector(state); - const userMetadata = pot.getOrElse(potUserMetadata, undefined); - - const localTabSections = selectedLocalServicesSectionsSelector(state); - const nationalTabSections = nationalServicesSectionsSelector(state); - const allTabSections = notSelectedServicesSectionsSelector(state); - - // All visibile services organized in sections - const allSections: ReadonlyArray = [ - ...localTabSections, - ...allTabSections - ]; - - const getTabSevicesId = (tabServices: ReadonlyArray) => - tabServices.reduce( - (acc: ReadonlyArray, curr: ServicesSectionState) => { - const sectionServices = curr.data.reduce( - ( - acc2: ReadonlyArray, - curr2: pot.Pot - ) => { - if (pot.isSome(curr2)) { - return [...acc2, curr2.value.service_id]; - } - return acc2; - }, - [] - ); - return [...acc, ...sectionServices]; - }, - [] - ); - - const tabsServicesId: { [k: number]: ReadonlyArray } = { - [0]: getTabSevicesId(nationalTabSections), - [1]: getTabSevicesId(localTabSections), - [2]: getTabSevicesId(allTabSections) - }; - - const visibleServicesContentLoadState = - visibleServicesDetailLoadStateSelector(state); - - const isLoadingServices = pot.isLoading(visibleServicesContentLoadState); - - const servicesLoadingFailure = - !pot.isLoading(potUserMetadata) && - pot.isError(visibleServicesContentLoadState); - - const loadDataFailure: DataLoadFailure = pot.isError(potUserMetadata) - ? "userMetadaLoadFailure" - : servicesLoadingFailure - ? "servicesLoadFailure" - : undefined; - - return { - debugONLYServices: servicesSelector(state), - isLoadingServices, - visibleServicesContentLoadState, - loadDataFailure, - profile: profileSelector(state), - visibleServices: visibleServicesSelector(state), - readServices: readServicesByIdSelector(state), - allSections, - localTabSections, - nationalTabSections, - allTabSections, - tabsServicesId, - wasServiceAlertDisplayedOnce: wasServiceAlertDisplayedOnceSelector(state), - servicesById: servicesByIdSelector(state), - potUserMetadata, - userMetadata, - isSearchEnabled: isSearchServicesEnabledSelector(state), - searchText: searchTextSelector(state) - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - navigateToServicePreference: () => navigateToServicePreferenceScreen(), - refreshUserMetadata: () => dispatch(userMetadataLoad.request()), - refreshVisibleServices: () => dispatch(loadVisibleServices.request()), - getServicesChannels: ( - servicesId: ReadonlyArray, - profile: ProfileState - ) => getChannelsforServicesList(servicesId, profile), - disableOrEnableServices: ( - servicesId: ReadonlyArray, - profile: ProfileState, - enable: boolean - ) => { - const newBlockedChannels = getProfileChannelsforServicesList( - servicesId, - profile, - enable - ); - dispatch( - profileUpsert.request({ - blocked_inbox_or_channels: newBlockedChannels - }) - ); - }, - saveSelectedOrganizationItems: ( - userMetadata: UserMetadata, - selectedItemIds: ReadonlyArray - ) => { - const metadata = userMetadata.metadata; - const currentVersion: number = userMetadata.version; - dispatch( - userMetadataUpsert.request({ - ...userMetadata, - version: currentVersion + 1, - metadata: { - ...metadata, - organizationsOfInterest: selectedItemIds - } - }) - ); - }, - navigateToServiceDetailsScreen: (params: ServiceDetailsScreenRouteParams) => - navigateToServiceDetailsScreen(params), - serviceDetailsLoad: (service: ServicePublic) => - dispatch(showServiceDetails(service)) -}); - -const mergeProps = ( - stateProps: ReturnType, - dispatchProps: ReturnType -) => { - // If the user updates the area of interest, the upsert of - // the user metadata stored on backend is triggered - const updateOrganizationsOfInterestMetadata = ( - selectedItemIds: O.Option> - ) => { - if (O.isSome(selectedItemIds) && stateProps.userMetadata) { - const updatedAreasOfInterest = Array.from(selectedItemIds.value); - dispatchProps.saveSelectedOrganizationItems( - stateProps.userMetadata, - updatedAreasOfInterest - ); - } - }; - - return { - ...stateProps, - ...dispatchProps, - ...{ - updateOrganizationsOfInterestMetadata - } - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps, - mergeProps -)(ServicesHomeScreen); diff --git a/ts/screens/services/ServicesLocalScreen.tsx b/ts/screens/services/ServicesLocalScreen.tsx deleted file mode 100644 index dbcf1cdf60f..00000000000 --- a/ts/screens/services/ServicesLocalScreen.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useCallback } from "react"; -import { View, StyleSheet } from "react-native"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import LocalServicesWebView from "../../components/services/LocalServicesWebView"; -import { showServiceDetails } from "../../store/actions/services"; -import { useIODispatch } from "../../store/hooks"; -import { useIONavigation } from "../../navigation/params/AppParamsList"; -import { SERVICES_ROUTES } from "../../features/services/common/navigation/routes"; - -const styles = StyleSheet.create({ - contentWrapper: { - flex: 1 - } -}); - -const ServicesLocalScreen = () => { - const dispatch = useIODispatch(); - const navigation = useIONavigation(); - - const onServiceSelect = useCallback( - (service: ServicePublic) => { - // when a service gets selected the service is recorded as read - dispatch(showServiceDetails(service)); - navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { - screen: SERVICES_ROUTES.SERVICE_DETAIL, - params: { serviceId: service.service_id } - }); - }, - [dispatch, navigation] - ); - - return ( - - {/* TODO: This is a workaround to make sure that the list is not placed under the tab bar - https://pagopa.atlassian.net/jira/software/projects/IOAPPFD0/boards/313?selectedIssue=IOAPPFD0-40 */} - - - - ); -}; - -export default ServicesLocalScreen; diff --git a/ts/screens/services/ServicesNationalScreen.tsx b/ts/screens/services/ServicesNationalScreen.tsx deleted file mode 100644 index 57d02bca73e..00000000000 --- a/ts/screens/services/ServicesNationalScreen.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as React from "react"; -import { useCallback } from "react"; -import { Animated } from "react-native"; -import { ServicePublic } from "../../../definitions/backend/ServicePublic"; -import ServicesTab from "../../components/services/ServicesTab"; -import { - loadVisibleServices, - showServiceDetails -} from "../../store/actions/services"; -import { userMetadataLoad } from "../../store/actions/userMetadata"; -import { useIODispatch, useIOSelector } from "../../store/hooks"; -import { - nationalServicesSectionsSelector, - visibleServicesDetailLoadStateSelector -} from "../../store/reducers/entities/services"; -import { userMetadataSelector } from "../../store/reducers/userMetadata"; -import { useIONavigation } from "../../navigation/params/AppParamsList"; -import { SERVICES_ROUTES } from "../../features/services/common/navigation/routes"; - -const tabScrollOffset = new Animated.Value(0); - -const ServicesNationalScreen = () => { - const navigation = useIONavigation(); - const dispatch = useIODispatch(); - const nationalTabSections = useIOSelector(nationalServicesSectionsSelector); - const visibleServicesContentLoadState = useIOSelector( - visibleServicesDetailLoadStateSelector - ); - const potUserMetadata = useIOSelector(userMetadataSelector); - const isLoadingServices = pot.isLoading(visibleServicesContentLoadState); - - const isRefreshing = - isLoadingServices || - pot.isLoading(potUserMetadata) || - pot.isUpdating(potUserMetadata); - - const refreshContent = useCallback(() => { - dispatch(userMetadataLoad.request()); - dispatch(loadVisibleServices.request()); - }, [dispatch]); - - const onServiceSelect = useCallback( - (service: ServicePublic) => { - // when a service gets selected the service is recorded as read - dispatch(showServiceDetails(service)); - navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { - screen: SERVICES_ROUTES.SERVICE_DETAIL, - params: { serviceId: service.service_id } - }); - }, - [dispatch, navigation] - ); - - return ( - - ); -}; - -export default ServicesNationalScreen; diff --git a/ts/screens/services/__tests__/ServiceDetailsScreen.test.tsx b/ts/screens/services/__tests__/ServiceDetailsScreen.test.tsx deleted file mode 100644 index d544ae2b9eb..00000000000 --- a/ts/screens/services/__tests__/ServiceDetailsScreen.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import configureMockStore from "redux-mock-store"; -import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { ServiceName } from "../../../../definitions/backend/ServiceName"; -import { OrganizationName } from "../../../../definitions/backend/OrganizationName"; -import { DepartmentName } from "../../../../definitions/backend/DepartmentName"; -import { applicationChangeState } from "../../../store/actions/application"; -import { appReducer } from "../../../store/reducers"; -import { GlobalState } from "../../../store/reducers/types"; -import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapper"; -import ServiceDetailsScreen from "../LegacyServiceDetailsScreen"; -import { loadServiceDetail } from "../../../features/services/details/store/actions/details"; -import { loadVisibleServices } from "../../../store/actions/services"; -import { SERVICES_ROUTES } from "../../../features/services/common/navigation/routes"; - -jest.useFakeTimers(); - -const service: ServicePublic = { - service_id: "A01" as ServiceId, - service_name: "ciao service" as ServiceName, - organization_name: "org" as OrganizationName, - department_name: "dep" as DepartmentName, - organization_fiscal_code: "12341234" as OrganizationFiscalCode, - version: 1 -}; - -describe("ServiceDetailsScreen", () => { - describe("when service's data load fails", () => { - it("should render the organization's fiscal code even if services list load is in failure", () => { - const { component, store } = renderComponent(); - store.dispatch(loadVisibleServices.failure(new Error("load failed"))); - expect( - component.getByText(service.organization_fiscal_code) - ).toBeDefined(); - }); - - it("should render the organization's fiscal code even if service detail load is in failure", () => { - const { component, store } = renderComponent(); - store.dispatch( - loadServiceDetail.failure({ - error: new Error("load failed"), - service_id: service.service_id - }) - ); - expect( - component.getByText(service.organization_fiscal_code) - ).toBeDefined(); - }); - }); -}); - -const renderComponent = () => { - const globalState = appReducer(undefined, applicationChangeState("active")); - - const mockStore = configureMockStore(); - const store: ReturnType = mockStore({ - ...globalState, - entities: { - ...globalState.entities, - services: { - ...globalState.entities.services, - byId: { - [service.service_id]: pot.some(service) - } - } - } - } as GlobalState); - - return { - component: renderScreenWithNavigationStoreContext( - ServiceDetailsScreen, - SERVICES_ROUTES.SERVICE_DETAIL, - { serviceId: service.service_id }, - store - ), - store - }; -}; diff --git a/ts/store/actions/navigation.ts b/ts/store/actions/navigation.ts index d794d1bdf6b..ece2334236b 100644 --- a/ts/store/actions/navigation.ts +++ b/ts/store/actions/navigation.ts @@ -116,16 +116,6 @@ export const navigateToServicesPreferenceModeSelectionScreen = ( * Service */ -/** - * @deprecated - */ -export const navigateToServiceHomeScreen = () => - NavigationService.dispatchNavigationAction( - CommonActions.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, { - screen: SERVICES_ROUTES.SERVICES_HOME - }) - ); - /** * @deprecated */ diff --git a/ts/store/actions/search.ts b/ts/store/actions/search.ts index f64ef6b1221..4b6a69986e7 100644 --- a/ts/store/actions/search.ts +++ b/ts/store/actions/search.ts @@ -9,10 +9,6 @@ export const searchMessagesEnabled = createStandardAction( "SEARCH_MESSAGES_ENABLED" )(); -export const searchServicesEnabled = createStandardAction( - "SEARCH_SERVICES_ENABLED" -)(); - export const updateSearchText = createStandardAction("UPDATE_SEARCH_TEXT")>(); @@ -20,6 +16,5 @@ export const disableSearch = createStandardAction("DISABLE_SEARCH")(); export type SearchActions = | ActionType - | ActionType | ActionType | ActionType; diff --git a/ts/store/actions/services/index.ts b/ts/store/actions/services/index.ts deleted file mode 100644 index c721203fcbf..00000000000 --- a/ts/store/actions/services/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Action types and action creator related to Services. - */ - -import { ITuple2 } from "@pagopa/ts-commons/lib/tuples"; -import { - ActionType, - createAsyncAction, - createStandardAction -} from "typesafe-actions"; - -import { PaginatedServiceTupleCollection } from "../../../../definitions/backend/PaginatedServiceTupleCollection"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; - -// -// service loading at startup -// - -export const firstServiceLoadSuccess = createStandardAction( - "FIRST_SERVICES_LOAD_SUCCESS" -)(); - -// -// load visible services -// - -export const loadVisibleServices = createAsyncAction( - "SERVICES_VISIBLE_LOAD_REQUEST", - "SERVICES_VISIBLE_LOAD_SUCCESS", - "SERVICES_VISIBLE_LOAD_FAILURE" -)(); - -// a specific action used when a requested service is not found -export const loadServiceDetailNotFound = createStandardAction( - "LOAD_SERVICE_DETAIL_NOT_FOUND" -)(); - -export const loadServicesDetail = createStandardAction( - "LOAD_SERVICES_DETAIL_REQUEST" -)>(); - -// -// mark service as read -// - -export const markServiceAsRead = createStandardAction( - "MARK_SERVICE_AS_READ" -)(); - -// -// show service detail -// - -export const showServiceDetails = createStandardAction( - "SERVICE_SHOW_DETAILS" -)(); - -// Remove services passing a list of tuples with serviceId and organizationFiscalCode -export const removeServiceTuples = createStandardAction( - "SERVICES_REMOVE_TUPLES" -)>>(); - -export type ServicesActions = - | ActionType - | ActionType - | ActionType - | ActionType - | ActionType - | ActionType - | ActionType; diff --git a/ts/store/actions/types.ts b/ts/store/actions/types.ts index dbe2e24f185..d50d671341e 100644 --- a/ts/store/actions/types.ts +++ b/ts/store/actions/types.ts @@ -22,7 +22,7 @@ import { AbiActions } from "../../features/wallet/onboarding/bancomat/store/acti import { BPayActions } from "../../features/wallet/onboarding/bancomatPay/store/actions"; import { CoBadgeActions } from "../../features/wallet/onboarding/cobadge/store/actions"; import { PayPalOnboardingActions } from "../../features/wallet/onboarding/paypal/store/actions"; -import { ServicesActions as NewServicesActions } from "../../features/services/common/store/actions"; +import { ServicesActions } from "../../features/services/common/store/actions"; import { WhatsNewActions } from "../../features/whatsnew/store/actions"; import { ZendeskSupportActions } from "../../features/zendesk/store/actions"; import { NotificationsActions } from "../../features/pushNotifications/store/actions/notifications"; @@ -53,7 +53,6 @@ import { PreferencesActions } from "./preferences"; import { ProfileActions } from "./profile"; import { ProfileEmailValidationAction } from "./profileEmailValidationChange"; import { SearchActions } from "./search"; -import { ServicesActions } from "./services"; import { StartupActions } from "./startup"; import { UserDataProcessingActions } from "./userDataProcessing"; import { UserMetadataActions } from "./userMetadata"; @@ -107,7 +106,6 @@ export type Action = | PaymentsFeatureActions | NewWalletActions | CieLoginConfigActions - | NewServicesActions | FimsActions | ItwActions | TrialSystemActions diff --git a/ts/store/reducers/entities/index.ts b/ts/store/reducers/entities/index.ts index 50216ec9b59..21525123719 100644 --- a/ts/store/reducers/entities/index.ts +++ b/ts/store/reducers/entities/index.ts @@ -24,12 +24,10 @@ import messagesStatusReducer, { import calendarEventsReducer, { CalendarEventsState } from "./calendarEvents"; import organizationsReducer, { OrganizationsState } from "./organizations"; import { paymentByRptIdReducer, PaymentByRptIdState } from "./payments"; -import servicesReducer, { ServicesState } from "./services"; export type EntitiesState = Readonly<{ messages: MessagesState; messagesStatus: MessagesStatus; - services: ServicesState; organizations: OrganizationsState; paymentByRptId: PaymentByRptIdState; calendarEvents: CalendarEventsState; @@ -37,17 +35,12 @@ export type EntitiesState = Readonly<{ export type PersistedEntitiesState = EntitiesState & PersistPartial; -const CURRENT_REDUX_ENTITIES_STORE_VERSION = 2; +const CURRENT_REDUX_ENTITIES_STORE_VERSION = 3; const migrations: MigrationManifest = { // version 0 // remove "currentSelectedService" section - "0": (state: PersistedState): PersistedEntitiesState => { - const entities = state as PersistedEntitiesState; - return { - ...entities, - services: { ..._.omit(entities.services, "currentSelectedService") } - }; - }, + "0": (state: PersistedState) => + _.omit(state, "services.currentSelectedService"), // version 1 // remove services section from persisted entities // TO avoid the proliferation of too many API requests until paged messages' API has been introduced @@ -67,7 +60,10 @@ const migrations: MigrationManifest = { ..._.omit(entities.messages, "allIds", "idsByServiceId", "byId") } }; - } + }, + // version 3 + // remove services from persisted entities + "3": (state: PersistedState) => _.omit(state, "services") }; // A custom configuration to avoid persisting messages section @@ -83,7 +79,6 @@ export const entitiesPersistConfig: PersistConfig = { const reducer = combineReducers({ messages: messagesReducer, messagesStatus: messagesStatusReducer, - services: servicesReducer, organizations: organizationsReducer, paymentByRptId: paymentByRptIdReducer, calendarEvents: calendarEventsReducer diff --git a/ts/store/reducers/entities/services/__tests__/index.test.ts b/ts/store/reducers/entities/services/__tests__/index.test.ts deleted file mode 100644 index 67a82bb73f9..00000000000 --- a/ts/store/reducers/entities/services/__tests__/index.test.ts +++ /dev/null @@ -1,337 +0,0 @@ -// TODO: it has to be updated due to https://www.pivotaltracker.com/story/show/169013940 - -// It implies item 42, not having the corresponding serviceMetadata being loaded, is not included among the local sections -// Check what happen with items 41 and 42 beign someLoading and someError -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { - NonEmptyString, - OrganizationFiscalCode -} from "@pagopa/ts-commons/lib/strings"; -import { - localServicesSectionsSelector, - nationalServicesSectionsSelector, - notSelectedServicesSectionsSelector, - organizationsOfInterestSelector, - servicesBadgeValueSelector, - ServicesSectionState, - ServicesState, - visibleServicesDetailLoadStateSelector -} from ".."; -import { DepartmentName } from "../../../../../../definitions/backend/DepartmentName"; -import { OrganizationName } from "../../../../../../definitions/backend/OrganizationName"; -import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; -import { ServiceName } from "../../../../../../definitions/backend/ServiceName"; -import { ServiceTuple } from "../../../../../../definitions/backend/ServiceTuple"; -import { UserMetadataState } from "../../../userMetadata"; -import { OrganizationsState } from "../../organizations"; -import { ServicesByIdState } from "../../../../../features/services/details/store/reducers/servicesById"; -import { VisibleServicesState } from "../visibleServices"; -import { ServiceScopeEnum } from "../../../../../../definitions/backend/ServiceScope"; -import { StandardServiceCategoryEnum } from "../../../../../../definitions/backend/StandardServiceCategory"; - -const customPotUserMetadata: UserMetadataState = pot.some({ - version: 1, - metadata: { - experimentalFeatures: true, - organizationsOfInterest: ["1", "2", "3", "4"] - } -}); - -const customServices: ServicesState = { - servicePreference: pot.none, - byId: { - ["11"]: pot.noneError(Error()), - ["21"]: pot.some({ - department_name: "test" as DepartmentName, - organization_fiscal_code: "2" as OrganizationFiscalCode, - organization_name: "organization2" as OrganizationName, - service_id: "21" as ServiceId, - service_name: "service1" as ServiceName, - version: 1, - service_metadata: { - category: StandardServiceCategoryEnum.STANDARD, - scope: ServiceScopeEnum.LOCAL - } - }), - ["22"]: undefined, - ["31"]: pot.someLoading({ - department_name: "test" as DepartmentName, - organization_fiscal_code: "3" as OrganizationFiscalCode, - organization_name: "organization3" as OrganizationName, - service_id: "31" as ServiceId, - service_name: "service1" as ServiceName, - version: 1 - }), - ["41"]: pot.someError( - { - department_name: "test" as DepartmentName, - organization_fiscal_code: "4" as OrganizationFiscalCode, - organization_name: "organization4" as OrganizationName, - service_id: "41" as ServiceId, - service_name: "service1" as ServiceName, - version: 1, - service_metadata: { - category: StandardServiceCategoryEnum.STANDARD, - - scope: ServiceScopeEnum.LOCAL - } - }, - Error("Generic error") - ), - ["42"]: pot.someLoading({ - department_name: "test" as DepartmentName, - organization_fiscal_code: "4" as OrganizationFiscalCode, - organization_name: "organization4" as OrganizationName, - service_id: "42" as ServiceId, - service_name: "service1" as ServiceName, - version: 1 - }), - ["43"]: pot.someLoading({ - department_name: "test" as DepartmentName, - organization_fiscal_code: "5" as OrganizationFiscalCode, - organization_name: "same_organization_name" as OrganizationName, - service_id: "43" as ServiceId, - service_name: "service1" as ServiceName, - version: 1 - }), - ["44"]: pot.someLoading({ - department_name: "test" as DepartmentName, - organization_fiscal_code: "6" as OrganizationFiscalCode, - organization_name: "same_organization_name" as OrganizationName, - service_id: "44" as ServiceId, - service_name: "service1" as ServiceName, - version: 1 - }) - }, - byOrgFiscalCode: { - ["2"]: ["21" as ServiceId, "22" as ServiceId] as ReadonlyArray, - ["3"]: ["31" as ServiceId] as ReadonlyArray, - ["4"]: ["41" as ServiceId, "42" as ServiceId] as ReadonlyArray, - ["5"]: ["43" as ServiceId] as ReadonlyArray, - ["6"]: ["44" as ServiceId] as ReadonlyArray - }, - visible: pot.some([ - { service_id: "11", version: 1 } as ServiceTuple, - { service_id: "21", version: 1 } as ServiceTuple, - { service_id: "22", version: 1 } as ServiceTuple, - { service_id: "41", version: 1 } as ServiceTuple, - { service_id: "43", version: 1 } as ServiceTuple, - { service_id: "44", version: 1 } as ServiceTuple - ]), - readState: { - ["21"]: true - }, - firstLoading: { - isFirstServicesLoadingCompleted: true - } -}; - -const customOrganizations: OrganizationsState = { - all: [ - { - name: "organizzazion2", - fiscalCode: "2" - }, - { - name: "organization3", - fiscalCode: "3" - }, - { - name: "organization4", - fiscalCode: "4" - }, - { - name: "same_organization_name", - fiscalCode: "5" - }, - { - name: "same_organization_name", - fiscalCode: "6" - } - ], - nameByFiscalCode: { - ["2" as OrganizationFiscalCode]: "organizzazion2" as NonEmptyString, - ["3" as OrganizationFiscalCode]: "organizzazion3" as NonEmptyString, - ["4" as OrganizationFiscalCode]: "organizzazion4" as NonEmptyString, - ["5" as OrganizationFiscalCode]: "same_organization_name" as NonEmptyString, - ["6" as OrganizationFiscalCode]: "same_organization_name" as NonEmptyString - } -}; - -const localServices: ReadonlyArray = [ - { - organizationName: customOrganizations.nameByFiscalCode["2"] as string, - organizationFiscalCode: "2" as OrganizationFiscalCode, - data: [customServices.byId["21"]] - } as ServicesSectionState, - { - organizationName: customOrganizations.nameByFiscalCode["4"] as string, - organizationFiscalCode: "4" as OrganizationFiscalCode, - data: [customServices.byId["41"]] - } as ServicesSectionState -]; - -const nationalServices: ReadonlyArray = []; - -describe("organizationsOfInterestSelector", () => { - it("should include organizations in the user organizationsOfInterest and providing visible services among those properly loaded", () => { - expect( - organizationsOfInterestSelector.resultFunc( - customPotUserMetadata, - customServices - ) - ).toStrictEqual(["2", "4"]); - }); -}); - -describe("nationalServicesSectionsSelector", () => { - it("should return the services having scope equal to NATIONAL", () => { - expect( - nationalServicesSectionsSelector.resultFunc( - customServices, - customOrganizations.nameByFiscalCode - ) - ).toStrictEqual(nationalServices); - }); -}); - -describe("localServicesSectionsSelector", () => { - it("should return the services having metadata and scope equal to LOCAL", () => { - expect( - localServicesSectionsSelector.resultFunc( - customServices, - customOrganizations.nameByFiscalCode - ) - ).toStrictEqual(localServices); - }); -}); - -describe("notSelectedServicesSectionsSelector", () => { - it("should return all the visible services with scope equal to both NATIONAL and LOCAL if the user organizationsOfInterest is empty", () => { - expect( - notSelectedServicesSectionsSelector.resultFunc( - customServices, - customOrganizations.nameByFiscalCode, - [""] - ) - ).toStrictEqual([ - { - organizationName: customOrganizations.nameByFiscalCode["2"] as string, - organizationFiscalCode: "2" as OrganizationFiscalCode, - data: [customServices.byId["21"]] - }, - { - organizationName: customOrganizations.nameByFiscalCode["4"] as string, - organizationFiscalCode: "4" as OrganizationFiscalCode, - data: [customServices.byId["41"]] - }, - { - organizationName: customOrganizations.nameByFiscalCode["5"] as string, - organizationFiscalCode: "5" as OrganizationFiscalCode, - data: [customServices.byId["43"], customServices.byId["44"]] - } - ]); - }); -}); - -describe("notSelectedServicesSectionsSelector", () => { - it("should return all the visible services with scope equal to both NATIONAL and LOCAL not included in organizationsOfInterest", () => { - expect( - notSelectedServicesSectionsSelector.resultFunc( - customServices, - customOrganizations.nameByFiscalCode, - ["4", "5"] // this organization has the same name of another organization (id:6) - ) - ).toStrictEqual([ - { - organizationName: customOrganizations.nameByFiscalCode["2"] as string, - organizationFiscalCode: "2" as OrganizationFiscalCode, - data: [customServices.byId["21"]] - } - ]); - }); -}); - -describe("visibleServicesDetailLoadStateSelector", () => { - it("should do be pot.noneLoading if at least one visible service is loading", () => { - expect( - visibleServicesDetailLoadStateSelector.resultFunc( - customServices.byId, - customServices.visible - ) - ).toBe(pot.noneLoading); - }); - - it("should do be pot.some when a service is loading but it is some", () => { - const data = { - byId: { - "azure-deployc49a": { - kind: "PotSomeLoading", - value: { - available_notification_channels: ["EMAIL", "WEBHOOK"], - department_name: "Progetto IO", - organization_fiscal_code: "15376371009", - organization_name: "IO - L'app dei servizi pubblici", - service_id: "azure-deployc49a", - service_name: "Novità e aggiornamenti", - version: 2 - } - } - }, - byOrgFiscalCode: { - "15376371009": ["azure-deployc49a"] - }, - visible: { - kind: "PotSome", - value: [ - { - scope: "NATIONAL", - service_id: "azure-deployc49a", - version: 1 - } - ] - } - }; - expect( - visibleServicesDetailLoadStateSelector.resultFunc( - data.byId as ServicesByIdState, - data.visible as VisibleServicesState - ) - ).toEqual(pot.some(undefined)); - }); -}); - -describe("servicesBadgeValueSelector", () => { - it("should return the number of unread services", () => { - expect( - servicesBadgeValueSelector.resultFunc( - [...nationalServices], - [...localServices], - customServices.readState, - true - ) - ).toBe(1); - }); - - it("should return 0 if the first load is not yet completed", () => { - expect( - servicesBadgeValueSelector.resultFunc( - [...nationalServices], - [...localServices], - customServices.readState, - false - ) - ).toBe(0); - }); - - it("should return 1 even if we have few duplication in services array", () => { - expect( - servicesBadgeValueSelector.resultFunc( - [...nationalServices], - [...localServices, ...localServices, ...localServices], - customServices.readState, - true - ) - ).toBe(1); - }); -}); diff --git a/ts/store/reducers/entities/services/__tests__/readStateByServiceId.test.ts b/ts/store/reducers/entities/services/__tests__/readStateByServiceId.test.ts deleted file mode 100644 index 47fa8c38a2e..00000000000 --- a/ts/store/reducers/entities/services/__tests__/readStateByServiceId.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; -import { appReducer } from "../../../index"; -import { - loadServiceDetailNotFound, - markServiceAsRead, - showServiceDetails -} from "../../../../actions/services"; -import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; -import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; -import { ServiceName } from "../../../../../../definitions/backend/ServiceName"; -import { NotificationChannelEnum } from "../../../../../../definitions/backend/NotificationChannel"; -import { OrganizationName } from "../../../../../../definitions/backend/OrganizationName"; -import { DepartmentName } from "../../../../../../definitions/backend/DepartmentName"; - -const mockService: ServicePublic = { - department_name: "dev department name" as DepartmentName, - organization_fiscal_code: "00000000001" as OrganizationFiscalCode, - organization_name: "Ramella, Zanetti and Maggiani [1]" as OrganizationName, - service_id: "id1" as ServiceId, - service_name: "reinventate next-generation architetture" as ServiceName, - available_notification_channels: [NotificationChannelEnum.EMAIL], - version: 1 -}; - -describe("readServicesByIdReducer", () => { - it("should be read", () => { - const state = appReducer(undefined, showServiceDetails(mockService)); - expect(state.entities.services.readState.id1).toBeTruthy(); - }); - - it("should be read", () => { - const state = appReducer(undefined, markServiceAsRead("id2" as ServiceId)); - expect(state.entities.services.readState.id2).toBeTruthy(); - }); - - it("should be undefined", () => { - const state = appReducer(undefined, showServiceDetails(mockService)); - expect(state.entities.services.readState.id3).toBeUndefined(); - }); - - it("should remove a specific not found service read state", () => { - const state1 = appReducer(undefined, markServiceAsRead("id1" as ServiceId)); - const state2 = appReducer(state1, markServiceAsRead("id2" as ServiceId)); - expect(state2.entities.services.readState.id1).toBeTruthy(); - expect(state2.entities.services.readState.id2).toBeTruthy(); - const updateState = appReducer( - state2, - loadServiceDetailNotFound("id1" as ServiceId) - ); - expect(updateState.entities.services.readState.id1).toBeUndefined(); - expect(state2.entities.services.readState.id2).toBeTruthy(); - }); -}); diff --git a/ts/store/reducers/entities/services/__tests__/servicesByOrganizationFiscalCode.test.ts b/ts/store/reducers/entities/services/__tests__/servicesByOrganizationFiscalCode.test.ts deleted file mode 100644 index 0c8212c3156..00000000000 --- a/ts/store/reducers/entities/services/__tests__/servicesByOrganizationFiscalCode.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Tuple2 } from "@pagopa/ts-commons/lib/tuples"; - -import { removeServiceTuples } from "../../../../actions/services"; -import { serviceIdsByOrganizationFiscalCodeReducer } from "../servicesByOrganizationFiscalCode"; - -const initialState = { - a: ["s1", "s2", "s3"], - b: ["s4", "s5", "s6", "s7"] -}; - -describe("servicesByOrganizationFiscalCode", () => { - it("should handle removeServiceTuples correctly", () => { - const action = removeServiceTuples([ - Tuple2("s2", "a"), - Tuple2("s3", "a"), - Tuple2("s6", "b"), - // Not existing serviceId - Tuple2("s8", "b"), - // Not existing organizationFiscalCode - Tuple2("s9", "c") - ]); - - const expectedState = { - a: ["s1"], - b: ["s4", "s5", "s7"] - }; - - const obtainedState = serviceIdsByOrganizationFiscalCodeReducer( - initialState as any, - action - ); - - expect(obtainedState).toMatchObject(expectedState); - }); -}); diff --git a/ts/store/reducers/entities/services/firstServicesLoading.ts b/ts/store/reducers/entities/services/firstServicesLoading.ts deleted file mode 100644 index 46c078f3a9d..00000000000 --- a/ts/store/reducers/entities/services/firstServicesLoading.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { getType } from "typesafe-actions"; -import { firstServiceLoadSuccess } from "../../../actions/services"; -import { Action } from "../../../actions/types"; -import { GlobalState } from "../../types"; - -export type FirstLoadingState = Readonly<{ - isFirstServicesLoadingCompleted: boolean; -}>; - -const INITIAL_STATE: FirstLoadingState = { - isFirstServicesLoadingCompleted: false -}; - -// Reducer -export const firstLoadingReducer = ( - state: FirstLoadingState = INITIAL_STATE, - action: Action -): FirstLoadingState => { - switch (action.type) { - case getType(firstServiceLoadSuccess): { - return { - isFirstServicesLoadingCompleted: true - }; - } - - default: - return state; - } -}; - -// Selectors -export const isFirstVisibleServiceLoadCompletedSelector = ( - state: GlobalState -) => state.entities.services.firstLoading.isFirstServicesLoadingCompleted; diff --git a/ts/store/reducers/entities/services/index.ts b/ts/store/reducers/entities/services/index.ts deleted file mode 100644 index 6a699761edd..00000000000 --- a/ts/store/reducers/entities/services/index.ts +++ /dev/null @@ -1,426 +0,0 @@ -/** - * Services reducer - */ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import { combineReducers } from "redux"; -import { createSelector } from "reselect"; -import { ServicePublic } from "../../../../../definitions/backend/ServicePublic"; -import { ServiceScopeEnum } from "../../../../../definitions/backend/ServiceScope"; -import servicePreferenceReducer, { - ServicePreferenceState -} from "../../../../features/services/details/store/reducers/servicePreference"; -import servicesByIdReducer, { - servicesByIdSelector, - ServicesByIdState -} from "../../../../features/services/details/store/reducers/servicesById"; -import { isDefined } from "../../../../utils/guards"; -import { isVisibleService } from "../../../../utils/services"; -import { Action } from "../../../actions/types"; -import { GlobalState } from "../../types"; -import { userMetadataSelector } from "../../userMetadata"; -import { - organizationNamesByFiscalCodeSelector, - OrganizationNamesByFiscalCodeState -} from "../organizations/organizationsByFiscalCodeReducer"; -import { - firstLoadingReducer, - FirstLoadingState, - isFirstVisibleServiceLoadCompletedSelector -} from "./firstServicesLoading"; -import readServicesByIdReducer, { - readServicesByIdSelector, - ReadStateByServicesId -} from "./readStateByServiceId"; -import { - serviceIdsByOrganizationFiscalCodeReducer, - ServiceIdsByOrganizationFiscalCodeState -} from "./servicesByOrganizationFiscalCode"; -import { - visibleServicesReducer, - visibleServicesSelector, - VisibleServicesState -} from "./visibleServices"; - -export type ServicesState = Readonly<{ - // Section to hold the preference for services - servicePreference: ServicePreferenceState; - byId: ServicesByIdState; - byOrgFiscalCode: ServiceIdsByOrganizationFiscalCodeState; - visible: VisibleServicesState; - readState: ReadStateByServicesId; - firstLoading: FirstLoadingState; -}>; - -export type ServicesSectionState = Readonly<{ - organizationName: string; - organizationFiscalCode: string; - data: ReadonlyArray>; -}>; - -const reducer = combineReducers({ - servicePreference: servicePreferenceReducer, - byId: servicesByIdReducer, - byOrgFiscalCode: serviceIdsByOrganizationFiscalCodeReducer, - visible: visibleServicesReducer, - readState: readServicesByIdReducer, - firstLoading: firstLoadingReducer -}); - -/** - * Selectors - */ - -export const servicesSelector = (state: GlobalState) => state.entities.services; - -/** - * The function returns: - * - pot.none if visibleServices is not loaded or the related services load is not yet started - * - pot.noneLoading if visibleServices or at least one visible service is loading - * - pot.noneError if visibleServices or at least one visible service load fails - * - pot.some if both visible services and all services load successfully - * @param visibleServices - list of visible services - * @param services - collection of services related data indexed with respect to the services id - * (applied for entities.services.byId state) - */ -function getServicesLoadState( - visibleServices: VisibleServicesState, - services: Readonly<{ - [key: string]: pot.Pot | undefined; - }> -): pot.Pot { - if (pot.isSome(visibleServices) && Object.keys(services).length > 0) { - const visibleServicesById = visibleServices.value.map( - service => services[service.service_id] - ); - - // check if there is at least one service in loading state - const areServicesLoading = - pot.isLoading(visibleServices) || - visibleServicesById.some( - vs => vs === undefined || (pot.isNone(vs) && pot.isLoading(vs)) - ); - - // check if there is at least one service in error state - const isServicesLoadFailed = - pot.isError(visibleServices) || - visibleServicesById.some( - service => service !== undefined && pot.isError(service) - ); - - if (areServicesLoading) { - return pot.noneLoading; - } else if (isServicesLoadFailed) { - return pot.noneError(Error(`Unable to load one or more services`)); - } else { - return pot.some(undefined); - } - } - - // If visibleServices is none - if (pot.isLoading(visibleServices)) { - return pot.noneLoading; - } else if (pot.isError(visibleServices)) { - return pot.noneError(Error("Unable to load visible services")); - } else { - return pot.none; - } -} - -// A selector to monitor the state of the service detail loading -export const visibleServicesDetailLoadStateSelector = createSelector( - [servicesByIdSelector, visibleServicesSelector], - (servicesById, visibleServices) => - getServicesLoadState(visibleServices, servicesById) -); - -/** - * A selector to get the organizations selected by the user as areas of interests - * which provide visible services - */ -export const organizationsOfInterestSelector = createSelector( - [userMetadataSelector, servicesSelector], - (potUserMetadata, services) => { - const visibleServices = new Set( - pot.getOrElse(services.visible, []).map(_ => _.service_id) - ); - - // If the user never select areas of interest, return an undefined object - return pot.toUndefined( - pot.map(potUserMetadata, _ => - // filter organization by selecting those ones having - // at least 1 visible service inside - _.metadata.organizationsOfInterest - ? _.metadata.organizationsOfInterest.filter(org => { - const organizationServices = services.byOrgFiscalCode[org] || []; - return organizationServices.some(serviceId => - visibleServices.has(serviceId) - ); - }) - : [] - ) - ); - } -); - -/** - * Functions and selectors to get services organized in sections - */ - -// Check if the passed service is local or national through data included into serviceByScope store item. -// If the scope parameter is expressed, the corresponding item is not included into the section if: -// - the scope parameter is different to the service scope -// - service detail or serviceByScope loading fails -const isInScope = ( - service: pot.Pot, - scope?: ServiceScopeEnum -) => { - if (scope === undefined) { - return true; - } - - // if service is in Error, the item is not included into the section - return pot.getOrElse( - pot.map(service, s => s.service_metadata?.scope === scope), - false - ); -}; - -// NOTE: this is a workaround not a solution -// since a service can change its organization fiscal code we could have -// obsolete data in the store: byOrgFiscalCode could have services that don't belong to organization anymore -// this cleaning its a workaround, this should be fixed on data loading and not when data are loaded -// see https://www.pivotaltracker.com/story/show/172316333 -/** - * return true if service belongs to the given organization fiscal code - * @param service - * @param organizationFiscalCode - */ -const belongsToOrganization = ( - service: pot.Pot, - organizationsFiscalCode: ReadonlyArray -) => - pot.getOrElse( - pot.map( - service, - s => organizationsFiscalCode.indexOf(s.organization_fiscal_code) !== -1 - ), - false - ); - -/** - * A generalized function to generate sections of organizations including the available services for each organization - * optional input: - * - scope: if undefined, all available organizations are included. If expressed, it requires service metadata being loaded - * - organizationsFiscalCodesSelected: if provided, sections will include only the passed organizations - */ -const getServices = ( - services: ServicesState, - organizations: OrganizationNamesByFiscalCodeState, - scope?: ServiceScopeEnum, - selectedOrganizationsFiscalCodes?: ReadonlyArray -) => { - const organizationsFiscalCodes = - selectedOrganizationsFiscalCodes === undefined - ? Object.keys(services.byOrgFiscalCode) - : selectedOrganizationsFiscalCodes; - // another workaround to avoid to display same organizations name that have different cf - // we group services by organization name - // to avoid duplication we keep in a set all organization fiscal code processed - const orgFiscalCodeProcessed = new Set(); - return organizationsFiscalCodes - .map((fiscalCode: string) => { - const organizationName = organizations[fiscalCode] || fiscalCode; - const organizationFiscalCode = fiscalCode; - if (orgFiscalCodeProcessed.has(fiscalCode)) { - return { - organizationName, - organizationFiscalCode, - data: [] - }; - } - - const orgsFiscalCodes = Object.keys(organizations).filter(cf => - pipe( - organizations[cf], - O.fromNullable, - O.fold( - () => false, - name => organizationName === name // select all services that belong to organizations having organizationName - ) - ) - ); - orgsFiscalCodes.forEach(ocf => orgFiscalCodeProcessed.add(ocf)); - const serviceIdsForOrg = orgsFiscalCodes.reduce( - (acc: ReadonlyArray, curr: string) => [ - ...acc, - ...(services.byOrgFiscalCode[curr] || []) - ], - [] - ); - - const data = serviceIdsForOrg - .map(id => services.byId[id]) - .filter( - service => - isDefined(service) && - belongsToOrganization(service, orgsFiscalCodes) && // workaround: see comments above this function definition - isInScope(service, scope) && - isVisibleService(services.visible, service) - ) - .sort((a, b) => - a && pot.isSome(a) && b && pot.isSome(b) - ? a.value.service_name - .toLocaleLowerCase() - .localeCompare(b.value.service_name.toLocaleLowerCase()) - : 0 - ); - - return { - organizationName, - organizationFiscalCode, - data - } as ServicesSectionState; - }) - .filter(_ => _.data.length > 0) - .sort((a, b) => - a.organizationName - .toLocaleLowerCase() - .localeCompare(b.organizationName.toLocaleLowerCase()) - ); -}; - -// A selector providing sections related to national services -export const nationalServicesSectionsSelector = createSelector( - [servicesSelector, organizationNamesByFiscalCodeSelector], - (services, organizations) => - getServices(services, organizations, ServiceScopeEnum.NATIONAL) -); - -// A selector providing sections related to local services -export const localServicesSectionsSelector = createSelector( - [servicesSelector, organizationNamesByFiscalCodeSelector], - (services, organizations) => - getServices(services, organizations, ServiceScopeEnum.LOCAL) -); - -// A selector providing sections related to the organizations selected by the user -export const selectedLocalServicesSectionsSelector = createSelector( - [ - servicesSelector, - organizationNamesByFiscalCodeSelector, - organizationsOfInterestSelector - ], - (services, organizations, selectedOrganizations) => - getServices(services, organizations, undefined, selectedOrganizations) -); - -// A selector providing sections related to: -// - all national services -// - local services not included into the user areas of interest -export const notSelectedServicesSectionsSelector = createSelector( - [ - servicesSelector, - organizationNamesByFiscalCodeSelector, - organizationsOfInterestSelector - ], - (services, organizations, selectedOrganizations) => { - const notSelectedOrganizations = pipe( - organizations, - O.fromNullable, - O.map(orgs => { - // add to organizations all cf of other organizations having the same organization name - const organizationsWithSameNames = pipe( - selectedOrganizations, - O.fromNullable, - O.map(so => - so.reduce((acc, curr) => { - const orgName = O.fromNullable(orgs[curr]); - return pipe( - orgName, - O.fold( - () => acc, - on => { - if (organizations !== undefined) { - const orgsFiscalCodes = Object.keys(organizations).filter( - cf => - pipe( - organizations[cf], - O.fromNullable, - O.fold( - () => false, - name => on === name // select all services that belong to organizations having organizationName - ) - ) - ); - orgsFiscalCodes.forEach(ofc => acc.add(ofc)); - } - return acc; - } - ) - ); - }, new Set()) - ), - O.fold( - () => [], - s => Array.from(s) - ) - ); - return Object.keys(orgs).filter( - fiscalCode => - organizationsWithSameNames && - organizationsWithSameNames.indexOf(fiscalCode) === -1 - ); - }) - ); - - return getServices( - services, - organizations, - undefined, - O.toUndefined(notSelectedOrganizations) - ); - } -); - -/** - * Get the sum of selected local services + national that are not yet marked as read - */ - -export const servicesBadgeValueSelector = createSelector( - [ - nationalServicesSectionsSelector, - selectedLocalServicesSectionsSelector, - readServicesByIdSelector, - isFirstVisibleServiceLoadCompletedSelector - ], - ( - nationalService, - localService, - readServicesById, - isFirstVisibleServiceLoadCompleted - ) => { - if (isFirstVisibleServiceLoadCompleted) { - const servicesSet: Set = new Set([ - ...nationalService, - ...localService - ]); - return [...servicesSet].reduce( - (acc: number, service: ServicesSectionState) => { - const servicesNotRead = service.data.filter( - data => - pot.isSome(data) && - readServicesById[data.value.service_id] === undefined - ).length; - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - return acc + servicesNotRead; - }, - 0 - ); - } - return 0; - } -); - -export default reducer; diff --git a/ts/store/reducers/entities/services/readStateByServiceId.ts b/ts/store/reducers/entities/services/readStateByServiceId.ts deleted file mode 100644 index b6bf43d7481..00000000000 --- a/ts/store/reducers/entities/services/readStateByServiceId.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getType } from "typesafe-actions"; -import _ from "lodash"; -import { - loadServiceDetailNotFound, - markServiceAsRead, - showServiceDetails -} from "../../../actions/services"; -import { Action } from "../../../actions/types"; -import { GlobalState } from "../../types"; -import { differentProfileLoggedIn } from "../../../actions/crossSessions"; - -export type ReadStateByServicesId = Readonly<{ - [key: string]: boolean | undefined; -}>; - -const INITIAL_STATE: ReadStateByServicesId = {}; - -// Selectors -export const readServicesByIdSelector = ( - state: GlobalState -): ReadStateByServicesId => state.entities.services.readState; - -// Reducer -export function readServicesByIdReducer( - state = INITIAL_STATE, - action: Action -): ReadStateByServicesId { - switch (action.type) { - case getType(showServiceDetails): - // add the service to the read list - return { - ...state, - [action.payload.service_id]: true - }; - - case getType(markServiceAsRead): - return { - ...state, - [action.payload]: true - }; - case getType(loadServiceDetailNotFound): - return _.omit(state, [action.payload]); - // reset reading state if current profile is different from the previous one - case getType(differentProfileLoggedIn): - return INITIAL_STATE; - - default: - return state; - } -} - -export default readServicesByIdReducer; diff --git a/ts/store/reducers/entities/services/servicesByOrganizationFiscalCode.ts b/ts/store/reducers/entities/services/servicesByOrganizationFiscalCode.ts deleted file mode 100644 index 417942c5a18..00000000000 --- a/ts/store/reducers/entities/services/servicesByOrganizationFiscalCode.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * A reducer to store the serviceIds by organization fiscal codes - */ - -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import { getType } from "typesafe-actions"; - -import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import { logoutSuccess, sessionExpired } from "../../../actions/authentication"; -import { loadServiceDetail } from "../../../../features/services/details/store/actions/details"; -import { removeServiceTuples } from "../../../actions/services"; -import { Action } from "../../../actions/types"; - -/** - * Maps organization fiscal code to serviceId - */ -export type ServiceIdsByOrganizationFiscalCodeState = Readonly<{ - [key: string]: ReadonlyArray | undefined; -}>; - -const INITIAL_STATE: ServiceIdsByOrganizationFiscalCodeState = {}; - -export function serviceIdsByOrganizationFiscalCodeReducer( - state: ServiceIdsByOrganizationFiscalCodeState = INITIAL_STATE, - action: Action -): ServiceIdsByOrganizationFiscalCodeState { - switch (action.type) { - case getType(loadServiceDetail.success): - const { organization_fiscal_code, service_id } = action.payload; - // get the current serviceIds for the organization fiscal code - const servicesForOrganization = state[organization_fiscal_code]; - - if ( - servicesForOrganization !== undefined && - servicesForOrganization.indexOf(service_id) >= 0 - ) { - // the service is already in the organization - return state; - } - - // add the service to the organization - const updatedServicesForOrganization = - servicesForOrganization === undefined - ? [service_id] - : [...servicesForOrganization, service_id]; - - return { - ...state, - [organization_fiscal_code]: updatedServicesForOrganization - }; - - case getType(logoutSuccess): - case getType(sessionExpired): - return INITIAL_STATE; - - case getType(removeServiceTuples): { - const serviceTuples = action.payload; - - // Remove service id from the array keyed by organizationFiscalCode - const stateUpdate = - serviceTuples.reduce( - (accumulator, tuple) => { - const serviceId = tuple.e1; - const organizationFiscalCode = tuple.e2; - const ids = pipe( - organizationFiscalCode, - O.fromNullable, - O.map(_ => - accumulator[_] !== undefined ? accumulator[_] : state[_] - ), - O.toNullable - ); - if (organizationFiscalCode && ids) { - const filteredIds = ids.filter(id => id !== serviceId); - return { - ...accumulator, - [organizationFiscalCode]: filteredIds - }; - } - return accumulator; - }, - {} - ); - - return { - ...state, - ...stateUpdate - }; - } - - default: - return state; - } -} diff --git a/ts/store/reducers/entities/services/transformers.ts b/ts/store/reducers/entities/services/transformers.ts index e8565fa1b99..333fd0790d6 100644 --- a/ts/store/reducers/entities/services/transformers.ts +++ b/ts/store/reducers/entities/services/transformers.ts @@ -1,5 +1,5 @@ import { ServicePublic } from "../../../../../definitions/backend/ServicePublic"; -import { logosForService } from "../../../../utils/services"; +import { logosForService } from "../../../../features/services/common/utils"; import { UIService } from "./types"; diff --git a/ts/store/reducers/entities/services/visibleServices.ts b/ts/store/reducers/entities/services/visibleServices.ts deleted file mode 100644 index 7a6dbe82c02..00000000000 --- a/ts/store/reducers/entities/services/visibleServices.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * A reducer to store the services normalized by id - * It only manages SUCCESS actions because all UI state properties (like * loading/error) - * are managed by different global reducers. - */ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { getType } from "typesafe-actions"; - -import { PaginatedServiceTupleCollection } from "../../../../../definitions/backend/PaginatedServiceTupleCollection"; -import { logoutSuccess, sessionExpired } from "../../../actions/authentication"; -import { loadVisibleServices } from "../../../actions/services"; -import { Action } from "../../../actions/types"; -import { GlobalState } from "../../types"; - -export type VisibleServicesState = pot.Pot< - PaginatedServiceTupleCollection["items"], - Error ->; - -const INITIAL_STATE: VisibleServicesState = pot.none; - -export const visibleServicesReducer = ( - state: VisibleServicesState = INITIAL_STATE, - action: Action -): VisibleServicesState => { - switch (action.type) { - case getType(loadVisibleServices.request): - return pot.toLoading(state); - - case getType(loadVisibleServices.success): - return pot.some(action.payload); - - case getType(loadVisibleServices.failure): - return pot.toError(state, action.payload); - - case getType(logoutSuccess): - case getType(sessionExpired): - return INITIAL_STATE; - - default: - return state; - } -}; - -// Selectors -export const visibleServicesSelector = ( - state: GlobalState -): VisibleServicesState => state.entities.services.visible; diff --git a/ts/store/reducers/index.ts b/ts/store/reducers/index.ts index 32b00065550..2476e37f967 100644 --- a/ts/store/reducers/index.ts +++ b/ts/store/reducers/index.ts @@ -212,7 +212,6 @@ export function createRootReducer( crossSessions: state.crossSessions, // data should be kept across multiple sessions entities: { - services: state.entities.services, organizations: state.entities.organizations, messagesStatus: state.entities.messagesStatus, paymentByRptId: state.entities.paymentByRptId, diff --git a/ts/store/reducers/search.ts b/ts/store/reducers/search.ts index 4ef9fa8beb9..8d7ee737013 100644 --- a/ts/store/reducers/search.ts +++ b/ts/store/reducers/search.ts @@ -7,7 +7,6 @@ import * as O from "fp-ts/lib/Option"; import { disableSearch, searchMessagesEnabled, - searchServicesEnabled, updateSearchText } from "../actions/search"; import { Action } from "../actions/types"; @@ -17,14 +16,12 @@ export type SearchState = Readonly<{ searchText: O.Option; isSearchEnabled: boolean; isSearchMessagesEnabled: boolean; - isSearchServicesEnabled: boolean; }>; const INITIAL_STATE: SearchState = { searchText: O.none, isSearchEnabled: false, - isSearchMessagesEnabled: false, - isSearchServicesEnabled: false + isSearchMessagesEnabled: false }; // Selectors @@ -37,9 +34,6 @@ export const isSearchEnabledSelector = (state: GlobalState): boolean => export const isSearchMessagesEnabledSelector = (state: GlobalState): boolean => state.search.isSearchMessagesEnabled; -export const isSearchServicesEnabledSelector = (state: GlobalState): boolean => - state.search.isSearchServicesEnabled; - const reducer = ( state: SearchState = INITIAL_STATE, action: Action @@ -52,13 +46,6 @@ const reducer = ( isSearchMessagesEnabled: action.payload }; - case getType(searchServicesEnabled): - return { - ...state, - isSearchEnabled: action.payload, - isSearchServicesEnabled: action.payload - }; - case getType(updateSearchText): return { ...state, searchText: action.payload }; diff --git a/ts/types/ServicesWebviewParams.ts b/ts/types/ServicesWebviewParams.ts deleted file mode 100644 index be8cee5c0bf..00000000000 --- a/ts/types/ServicesWebviewParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * this type models an object that represents the params needed by the ServicesWebviewScreen to work - * see https://www.pivotaltracker.com/story/show/174801117 - */ -import * as t from "io-ts"; -import { ServiceId } from "../../definitions/backend/ServiceId"; - -export const ServicesWebviewParams = t.interface({ - serviceId: ServiceId, - url: t.string -}); - -export type ServicesWebviewParams = t.TypeOf; diff --git a/ts/types/WebviewMessage.ts b/ts/types/WebviewMessage.ts index 207f7eabdea..fc61c8ab8e6 100644 --- a/ts/types/WebviewMessage.ts +++ b/ts/types/WebviewMessage.ts @@ -1,5 +1,5 @@ /** - * these models describe the incoming data sent from web pages that include app injected JS (see RegionServiceWebView) + * these models describe the incoming data sent from web pages that include app injected JS */ import * as t from "io-ts"; diff --git a/ts/types/__tests__/ServicesWebviewParams.test.ts b/ts/types/__tests__/ServicesWebviewParams.test.ts deleted file mode 100644 index 79a90a837d0..00000000000 --- a/ts/types/__tests__/ServicesWebviewParams.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as E from "fp-ts/lib/Either"; -import { ServicesWebviewParams } from "../ServicesWebviewParams"; - -const validParams = { - serviceId: "service_id", - url: "http://google.com" -}; - -const invalidParams1 = { - url: "http://google.com" -}; - -const invalidParams2 = { - serviceId: "service_id" -}; - -const invalidParams3 = { - serviceId: "", - url: "http://google.com" -}; - -describe("WebviewMessage", () => { - it("Should recognize a valid payload for Params", () => { - expect(E.isRight(ServicesWebviewParams.decode(validParams))).toBeTruthy(); - }); - - it("Should recognize an invalid payload for Params", () => { - expect(E.isRight(ServicesWebviewParams.decode(invalidParams1))).toBeFalsy(); - }); - - it("Should recognize an invalid payload for Params", () => { - expect(E.isRight(ServicesWebviewParams.decode(invalidParams2))).toBeFalsy(); - }); - - it("Should recognize an invalid payload for Params", () => { - expect(E.isRight(ServicesWebviewParams.decode(invalidParams3))).toBeFalsy(); - }); -}); diff --git a/ts/utils/analytics.ts b/ts/utils/analytics.ts index 0e1e6afeb3a..f70ef947016 100644 --- a/ts/utils/analytics.ts +++ b/ts/utils/analytics.ts @@ -10,7 +10,6 @@ import EUCOVIDCERT_ROUTES from "../features/euCovidCert/navigation/routes"; import { euCovidCertificateEnabled } from "../config"; import { mixpanelTrack } from "../mixpanel"; import { isLoginUtilsError } from "../features/lollipop/utils/login"; -import { ServicesDetailLoadTrack } from "../sagas/startup/loadServiceDetailRequestHandler"; const blackListRoutes: ReadonlyArray = []; @@ -93,20 +92,6 @@ export const buildEventProperties = ( flow }); -// Services related events - -export function trackServiceDetailLoadingStatistics( - trackingStats: ServicesDetailLoadTrack -) { - void mixpanelTrack("SERVICES_DETAIL_LOADING_STATS", { - ...trackingStats, - // drop servicesId since it is not serialized in mixpanel and it could be an extra overhead on sending - servicesId: undefined - }); -} - -// End of Services related events - // Lollipop events export function trackLollipopKeyGenerationSuccess(keyType?: string) { void mixpanelTrack("LOLLIPOP_KEY_GENERATION_SUCCESS", { diff --git a/ts/utils/organizations.ts b/ts/utils/organizations.ts deleted file mode 100644 index 614702c096c..00000000000 --- a/ts/utils/organizations.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Generic utilities for organizations - */ -import { contentRepoUrl } from "../config"; - -/** - * This is a partial duplication of ./services/logosForService. - * TODO: remove it in favour of the generic one - * - * @deprecated - * @param orgFiscalCode - * @param logosRepoUrl - */ -export function getLogoForOrganization( - orgFiscalCode: string, - logosRepoUrl: string = `${contentRepoUrl}/logos` -) { - return [`organizations/${orgFiscalCode.replace(/^0+/, "")}`].map(u => ({ - uri: `${logosRepoUrl}/${u}.png` - })); -} diff --git a/ts/utils/profile.ts b/ts/utils/profile.ts index 166d4caee0e..3ec42c92ba4 100644 --- a/ts/utils/profile.ts +++ b/ts/utils/profile.ts @@ -1,12 +1,6 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import { BlockedInboxOrChannels } from "../../definitions/backend/BlockedInboxOrChannels"; import { FiscalCode } from "../../definitions/backend/FiscalCode"; -import { InitializedProfile } from "../../definitions/backend/InitializedProfile"; -import { ServiceId } from "../../definitions/backend/ServiceId"; import { Municipality } from "../../definitions/content/Municipality"; -import { ProfileState } from "../store/reducers/profile"; import { pad } from "./dates"; type GenderType = "M" | "F" | undefined; @@ -103,142 +97,3 @@ export interface EnabledChannels { push: boolean; can_access_message_read_status: boolean; } - -const INBOX_CHANNEL = "INBOX"; -const EMAIL_CHANNEL = "EMAIL"; -const PUSH_CHANNEL = "WEBHOOK"; -const SEND_READ_MESSAGE_STATUS_CHANNEL = "SEND_READ_MESSAGE_STATUS_CHANNEL"; - -export function getChannelsforServicesList( - servicesId: ReadonlyArray, - profile: ProfileState -): BlockedInboxOrChannels { - const profileBlockedChannels = pot.getOrElse( - pot.mapNullable(profile, up => up.blocked_inbox_or_channels), - {} as BlockedInboxOrChannels - ); - - return servicesId.reduce( - (acc, serviceId) => ({ - ...acc, - [serviceId]: profileBlockedChannels[serviceId] || [] - }), - {} as BlockedInboxOrChannels - ); -} - -/** - * Provide new BlockedInboxOrChannels object to disable - * or enable (if enableListedServices is true) - * a list of services (listed as servicesId). - * If not declared, the enabled/disabled channel is the INBOX, - * otherwise it is updated the channel expressed as channelOfInterest - */ -export function getProfileChannelsforServicesList( - servicesId: ReadonlyArray, - profile: ProfileState, - enableListedServices: boolean, - channelOfInterest: string = INBOX_CHANNEL -): BlockedInboxOrChannels { - const profileBlockedChannels = pot.getOrElse( - pot.mapNullable( - profile, - userProfile => userProfile.blocked_inbox_or_channels - ), - {} as BlockedInboxOrChannels - ); - - servicesId.forEach(id => { - const channels = - Object.keys(profileBlockedChannels).indexOf(id) !== -1 - ? profileBlockedChannels[id] - : []; - - const updatedBlockedChannels = - channels.indexOf(channelOfInterest) === -1 - ? enableListedServices - ? channels - : channels.concat(channelOfInterest) - : enableListedServices - ? channels.filter(item => item !== channelOfInterest) - : channels; - - if (updatedBlockedChannels.length !== 0) { - // eslint-disable-next-line - profileBlockedChannels[id] = updatedBlockedChannels; - } else { - // eslint-disable-next-line - delete profileBlockedChannels[id]; - } - }); - - return profileBlockedChannels; -} - -/** - * Finds out which channels are enabled in the profile for the provided service - */ -export function getEnabledChannelsForService( - potProfile: ProfileState, - serviceId: ServiceId -): EnabledChannels { - return pipe( - pot.toOption(potProfile), - O.chainNullableK(profile => - InitializedProfile.is(profile) ? profile.blocked_inbox_or_channels : null - ), - O.chainNullableK(blockedChannels => blockedChannels[serviceId]), - O.map(_ => ({ - inbox: _.indexOf(INBOX_CHANNEL) === -1, - email: _.indexOf(EMAIL_CHANNEL) === -1, - push: _.indexOf(PUSH_CHANNEL) === -1, - can_access_message_read_status: - _.indexOf(SEND_READ_MESSAGE_STATUS_CHANNEL) === -1 - })), - O.getOrElseW(() => ({ - inbox: true, - email: true, - push: true, - can_access_message_read_status: true - })) - ); -} - -/** - * Returns a function that generates updated blocked channels from the - * enabled channels of one service - */ -export const getBlockedChannels = - (potProfile: ProfileState, serviceId: ServiceId) => - (enabled: EnabledChannels): BlockedInboxOrChannels => { - // get the current blocked channels from the profile - const profileBlockedChannels = pot.getOrElse( - pot.mapNullable( - potProfile, - userProfile => userProfile.blocked_inbox_or_channels - ), - {} as BlockedInboxOrChannels - ); - - // compute the blocked channels array for this service - const blockedChannelsForService = [ - !enabled.inbox ? INBOX_CHANNEL : "", - !enabled.push ? PUSH_CHANNEL : "", - !enabled.email ? EMAIL_CHANNEL : "", - !enabled.can_access_message_read_status - ? SEND_READ_MESSAGE_STATUS_CHANNEL - : "" - ].filter(_ => _ !== ""); - - if (blockedChannelsForService.length === 0) { - // eslint-disable-next-line functional/immutable-data - delete profileBlockedChannels[serviceId]; - } else { - // eslint-disable-next-line functional/immutable-data - profileBlockedChannels[serviceId] = blockedChannelsForService; - } - - return { - ...profileBlockedChannels - }; - }; diff --git a/ts/utils/services.ts b/ts/utils/services.ts deleted file mode 100644 index 3dafbf24e50..00000000000 --- a/ts/utils/services.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Generic utilities for services - */ - -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { ImageURISource } from "react-native"; -import { ServicePublic } from "../../definitions/backend/ServicePublic"; -import { contentRepoUrl } from "../config"; -import { VisibleServicesState } from "../store/reducers/entities/services/visibleServices"; -import { isTextIncludedCaseInsensitive } from "./strings"; - -/** - * Returns an array of ImageURISource pointing to possible logos for the - * provided service. - * - * The returned array is suitable for being used with the MultiImage component. - * The arrays will have first the service logo, then the organization logo. - */ -export function logosForService( - service: ServicePublic, - logosRepoUrl: string = `${contentRepoUrl}/logos` -): ReadonlyArray { - return [ - `services/${service.service_id.toLowerCase()}`, - `organizations/${service.organization_fiscal_code.replace(/^0+/, "")}` - ].map(u => ({ - uri: `${logosRepoUrl}/${u}.png` - })); -} - -export function serviceContainsText( - service: ServicePublic, - searchText: string -) { - return ( - isTextIncludedCaseInsensitive(service.department_name, searchText) || - isTextIncludedCaseInsensitive(service.organization_name, searchText) || - isTextIncludedCaseInsensitive(service.service_name, searchText) - ); -} - -// Return true if the given service is available (visible) -export const isVisibleService = ( - visibleServices: VisibleServicesState, - potService: pot.Pot -) => { - const service = pot.toUndefined(potService); - return ( - service && - pot.getOrElse( - pot.map(visibleServices, services => - services.some(item => service.service_id === item.service_id) - ), - false - ) - ); -}; From 9e7c094326cbd91e0ef78fc7ae5f2ce323ed6966 Mon Sep 17 00:00:00 2001 From: Alessandro Dell'Oste Date: Tue, 25 Jun 2024 11:46:06 +0200 Subject: [PATCH 4/9] Removed unused translation keys --- locales/de/index.yml | 51 ------------------------------------------- locales/en/index.yml | 52 -------------------------------------------- locales/it/index.yml | 52 -------------------------------------------- 3 files changed, 155 deletions(-) diff --git a/locales/de/index.yml b/locales/de/index.yml index 8330436160b..ed7068d2d89 100644 --- a/locales/de/index.yml +++ b/locales/de/index.yml @@ -1871,8 +1871,6 @@ send_email_messages: label: "Dienst für Dienst auswählen" info: "Du kannst auf der Registerkarte jedes Dienstes wählen, ob du dessen Mitteilungen auch per E-Mail erhalten möchtest" services: - accessibility: - edit: "Ändere die Einstellung der Dienste" optIn: preferences: completed: @@ -1903,40 +1901,7 @@ services: title: "Wenn du bestätigst, erhältst du keine Mitteilungen" body: "Bei einer manuellen Konfiguration können dir weder aktuelle noch zukünftige IO-Dienste Mitteilungen senden. Damit Dienste, an denen du interessiert bist, dich kontaktieren können, musst du sie einzeln konfigurieren, indem du den Punkt “In der App kontaktieren” auf jeder Registerkarte des Dienstes aktivierst." title: "Dienste" - subTitle: "Aktiviere oder deaktiviere die Dienste, die dir Mitteilungen senden dürfen" contextualHelpTitle: "Was du in deinen Diensten machen kannst" - serviceIsEnabled: "In der App kontaktieren" - serviceNotEnabled: "Der Dienst ist nicht aktiv" - pushNotifications: "Push-Benachrichtigungen senden" - messageReadStatus: "Lesebestätigungen erhalten" - emailForwarding: "E-Mails senden" - tosAndPrivacy: "Nutzungsbedingungen und Datenschutzbestimmungen" - tosLink: "Nutzungsbedingungen" - privacyLink: "Informationen zum Datenschutz" - otherAppsInfo: "Du findest diesen Dienst auch" - otherAppWeb: "online auf" - otherAppIos: "iOS-App" - otherAppAndroid: "Android-App" - contactsAndInfo: "Kontakte und Informationen" - visitWebsite: "Website" - askForAssistance: "Hilfe anfordern" - contactAddress: "Adresse" - contactPhone: "Telefon" - contactSupport: "Support" - tab: - locals: "Lokal" - national: "National" - all: "Alle" - loading: - title: "Lade die Liste der Dienste" - subtitle: "Warte ein paar Sekunden..." - enableAll: "Schnelleinrichtung verwenden" - disableAll: "Manuelle Konfiguration verwenden" - updatingServiceMode: "Bitte warten..." - disableAllTitle: "Möchtest du wirklich alle Dienste deaktivieren?" - disableAllMsg: "Bedenke, dass du nur von Diensten angeschrieben wirst, die tatsächlich über persönliche Informationen verfügen, die sie dir mitteilen möchten. Du wirst keine Spam-Mitteilungen erhalten. Wenn du alle Dienste deaktivierst, können diese dich nicht mehr über IO kontaktieren (bis du sie wieder aktivierst). Du kannst die Dienste weiterhin über andere Kanäle nutzen (Schalter, Website usw.)." - close: "Schließen" - emptyListMessage: "Zurzeit sind keine Dienste verfügbar, zum Aktualisieren ziehe auf dem Bildschirm nach unten" home: institutions: title: "National" @@ -1966,22 +1931,6 @@ services: title: "Nutzungsbedingungen und Datenschutzbestimmungen" tosLink: "Nutzungsbedingungen" privacyLink: "Datenschutzerklärung" -serviceDetail: - fiscalCode: "Steuernummer Körperschaft" - fiscalCodeAccessibility: "Steuernummer Körperschaft" - fiscalCodeAccessibilityCopy: "Kopiere Steuernummer Körperschaft" - contacts: - title: "Dieser Dienst kann:" - headerTitle: "Details zum Dienst" - onUpdateEnabledChannelsFailure: "Beim Speichern der Einstellungen ist ein temporärer Fehler aufgetreten. Bitte versuche es erneut." - disableTitle: "Möchtest du den Dienst wirklich deaktivieren?" - disableMsg: "Bedenke, dass du nur von Diensten angeschrieben wirst, die tatsächlich über persönliche Informationen verfügen, die sie dir mitteilen möchten. Du wirst keine Spam-Mitteilungen erhalten. Wenn du diesen Dienst deaktivierst, kann dieser dich nicht mehr über IO kontaktieren (bis du ihn wieder aktivierst). Du kannst diesen Dienst weiterhin über andere Kanäle nutzen (Schalter, Website usw.)." - lockedMailAlert: "Die E-Mail-Weiterleitung von Mitteilungen ist global {{enabled}}:" - updatePreferences: "Einstellungen ändern" - goTo: "Geh zu den Einstellungen" - enabled: "aktiviert" - disabled: "deaktiviert" - notValidated: "Um die E-Mail-Weiterleitung zu aktivieren, musst du deine E-Mail-Adresse bestätigen." identification: instructions: unlockCode: "Entsperrcode" diff --git a/locales/en/index.yml b/locales/en/index.yml index 277a8774868..5478f1d875c 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -2090,8 +2090,6 @@ send_email_messages: label: Select service by service info: You will be able to select from each service settings screen to receive its messages by email as well services: - accessibility: - edit: Edit services setup optIn: preferences: completed: @@ -2122,41 +2120,8 @@ services: title: "If you confirm, you won't receive any message" body: "With manual configuration the services on IO, present and future, will not be able to contact you. To allow the services you are interested in to contact you, you will have to configure them one by one by enabling “Contact you in app” in each service detail page." title: Services - subTitle: Enable or disable the services that are allowed to send you messages contextualHelpTitle: About this section contextualHelpContent: !include services/services_home.md - serviceIsEnabled: Contact you in app - serviceNotEnabled: The service is not enabled - pushNotifications: Send you push notifications - messageReadStatus: Receive read receipts - emailForwarding: Send you e-mail - tosAndPrivacy: Terms of use and Privacy - tosLink: Terms and conditions - privacyLink: Privacy information - otherAppsInfo: You can find this service also - otherAppWeb: Online on - otherAppIos: iOS App - otherAppAndroid: Android App - contactsAndInfo: Contacts and info - visitWebsite: Browse Website - askForAssistance: Get support - contactAddress: Address - contactPhone: Phone - contactSupport: Support - tab: - locals: Local - national: National - all: All - loading: - title: Loading the services list - subtitle: Please wait... - enableAll: Use quick setup - disableAll: Use manual setup - updatingServiceMode: Please wait... - disableAllTitle: "Do you really want to disable all the services?" - disableAllMsg: "Remember, on IO you will receive messages only from services having some personalized information to communicate to you. You will not receive spam messages. If you disable all the services, they can no longer contact you through the app (until you enable it again). You can use the services through other channels (front office, website, etc.)" - close: Close - emptyListMessage: There are no services available at this time, pull down to refresh new: New home: featured: @@ -2210,23 +2175,6 @@ services: title: "Terms of use and Privacy" tosLink: "Terms and conditions" privacyLink: "Privacy information" -serviceDetail: - fiscalCode: "Institution's fiscal code" - fiscalCodeAccessibility: "Institution's fiscal code" - fiscalCodeAccessibilityCopy: "Copy Institution's fiscal code" - contacts: - title: "This service can:" - headerTitle: "Service details" - contextualHelpContent: !include services/service_detail.md - onUpdateEnabledChannelsFailure: "There was a temporary problem while saving preferences, please retry." - disableTitle: "Do you really want to disable the service?" - disableMsg: "Remember, on IO you will receive messages only from services having some personalized information to communicate to you. You will not receive spam messages. If you disable the service, it can no longer contact you by IO (until you enable it again). You will be able to use the service by other channels (front office, website, etc.)" - lockedMailAlert: "The email forwarding of messages is globally {{enabled}}:" - updatePreferences: Update your preferences - goTo: Go to preferences - enabled: enabled - disabled: disabled - notValidated: To enable the email forwarding you must validate your email address. identification: instructions: unlockCode: unlock code diff --git a/locales/it/index.yml b/locales/it/index.yml index 651f3f58320..1b26aa69288 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -2090,8 +2090,6 @@ send_email_messages: label: Scegli servizio per servizio info: Potrai scegliere nella scheda di ogni servizio di ricevere i relativi messaggi anche via email services: - accessibility: - edit: Modifica l'impostazione dei servizi optIn: preferences: completed: @@ -2122,41 +2120,8 @@ services: title: "Se confermi, non riceverai alcun messaggio" body: "Con la configurazione manuale i servizi di IO, presenti o futuri, non possono inviarti messaggi. Per permettere ai servizi che ti interessano di contattarti, dovrai configurarli uno a uno abilitando la voce “Contattarti in app” che trovi in ogni scheda servizio." title: Servizi - subTitle: Attiva o disattiva i servizi da cui puoi ricevere messaggi contextualHelpTitle: Cosa puoi fare nei tuoi Servizi contextualHelpContent: !include services/services_home.md - serviceIsEnabled: Contattarti in app - serviceNotEnabled: Il servizio non è attivo - pushNotifications: Inviarti notifiche push - messageReadStatus: Ricevere conferme di lettura - emailForwarding: Inviarti e-mail - tosAndPrivacy: Termini e Privacy - tosLink: Termini e condizioni d'uso - privacyLink: Informativa sulla privacy - otherAppsInfo: Puoi usare questo servizio anche - otherAppWeb: Online su - otherAppIos: App iOS - otherAppAndroid: App Android - contactsAndInfo: Contatti ed informazioni - visitWebsite: Visita il sito - askForAssistance: Richiedi assistenza - contactAddress: Indirizzo - contactPhone: Telefono - contactSupport: Assistenza - tab: - locals: Locali - national: Nazionali - all: Tutti - loading: - title: Sto caricando l'elenco dei servizi - subtitle: attendi qualche secondo... - enableAll: Usa configurazione rapida - disableAll: Usa configurazione manuale - updatingServiceMode: Attendi... - disableAllTitle: Vuoi davvero disattivare tutti i servizi? - disableAllMsg: "Ricorda che su IO ti scriveranno solo i servizi che effettivamente hanno qualche informazione personalizzata da comunicarti. Non riceverai messaggi di spam. Se disattivi tutti i servizi, questi non potranno più contattarti tramite IO (finché non li riattiverai). Potrai continuare a fruire i servizi attraverso altri canali (sportello, sito, etc.)" - close: Chiudi - emptyListMessage: Non ci sono servizi disponibili al momento, trascina in basso per aggiornare new: Nuovo home: featured: @@ -2210,23 +2175,6 @@ services: title: "Termini e Privacy" tosLink: "Termini e condizioni d'uso" privacyLink: "Informativa sulla privacy" -serviceDetail: - fiscalCode: "C.F. Ente" - fiscalCodeAccessibility: "Codice fiscale Ente" - fiscalCodeAccessibilityCopy: "Copia codice fiscale Ente" - contacts: - title: "Questo servizio può:" - headerTitle: "Dettagli del Servizio" - contextualHelpContent: !include services/service_detail.md - onUpdateEnabledChannelsFailure: "Si è verificato un errore temporaneo nel salvataggio delle preferenze, riprova per piacere." - disableTitle: "Vuoi davvero disattivare il servizio?" - disableMsg: "Ricorda che su IO ti scriveranno solo i servizi che effettivamente hanno qualche informazione personalizzata da comunicarti. Non riceverai messaggi di spam. Se disattivi il servizio, questo non potrà più contattarti tramite IO (finché non lo riattiverai). Potrai continuare a fruire il servizio attraverso altri canali (sportello, sito, etc.)" - lockedMailAlert: "L'inoltro dei messaggi via email è {{enabled}} globalmente:" - updatePreferences: Modifica le preferenze - goTo: Vai alle preferenze - enabled: abilitato - disabled: disabilitato - notValidated: Per poter abilitare le notifiche via email devi validare il tuo indirizzo email." identification: instructions: unlockCode: codice di sblocco From 5ad155d15f885d4f490c25e7fbe0b624b7a536c1 Mon Sep 17 00:00:00 2001 From: Alessandro Dell'Oste Date: Tue, 25 Jun 2024 12:01:08 +0200 Subject: [PATCH 5/9] Merge branch 'master' into IOPAE-1100-1282-1285 --- locales/de/index.yml | 11 - locales/en/index.yml | 15 +- locales/it/index.yml | 15 +- ts/navigation/ProfileNavigator.tsx | 3 - ts/screens/profile/EmailForwardingScreen.tsx | 350 +++++++------------ 5 files changed, 128 insertions(+), 266 deletions(-) diff --git a/locales/de/index.yml b/locales/de/index.yml index ed7068d2d89..15206a136c2 100644 --- a/locales/de/index.yml +++ b/locales/de/index.yml @@ -1859,17 +1859,6 @@ biometric_recognition: needed_to_disable: "Bestätigung zur Deaktivierung erforderlich" send_email_messages: title: "Mitteilungen mittels E-Mail weiterleiten" - subtitle: "Möchtest du, dass IO den Inhalt von Mitteilungen per E-Mail weiterleitet an" - options: - disable_all: - label: "Für alle Dienste deaktivieren" - info: "Du erhältst keine Mitteilung mittels E-Mail mehr" - enable_all: - label: "Für alle aktiven Dienste aktivieren" - info: "Jede Mitteilung wird an deine E-Mail-Adresse weitergeleitet, wenn der Dienst dies zulässt" - by_service: - label: "Dienst für Dienst auswählen" - info: "Du kannst auf der Registerkarte jedes Dienstes wählen, ob du dessen Mitteilungen auch per E-Mail erhalten möchtest" services: optIn: preferences: diff --git a/locales/en/index.yml b/locales/en/index.yml index 5478f1d875c..0158a6b4068 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -2078,17 +2078,10 @@ biometric_recognition: needed_to_disable: Recognition required to disable send_email_messages: title: Forward messages by email - subtitle: Do you want IO to forward messages contents by email to - options: - disable_all: - label: Disable for all services - info: You will not receive email messages anymore - enable_all: - label: Enable for all active services - info: Every message will be forwarded to your email, if allowed by the service - by_service: - label: Select service by service - info: You will be able to select from each service settings screen to receive its messages by email as well + subtitle: You want IO to forward message previews to + switch: + title: Email a preview + subtitle: You will get a preview of the message at your email address if the service allows. services: optIn: preferences: diff --git a/locales/it/index.yml b/locales/it/index.yml index 1b26aa69288..212f8e99606 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -2078,17 +2078,10 @@ biometric_recognition: needed_to_disable: Riconoscimento richiesto per disabilitare send_email_messages: title: Inoltro dei messaggi via email - subtitle: Vuoi che IO inoltri il contenuto dei messaggi anche a - options: - disable_all: - label: Disabilita per tutti i servizi - info: Non riceverai più messaggi via email - enable_all: - label: Abilita per tutti i servizi attivi - info: Ogni messaggio sarà inoltrato alla tua email, se il servizio lo consente - by_service: - label: Scegli servizio per servizio - info: Potrai scegliere nella scheda di ogni servizio di ricevere i relativi messaggi anche via email + subtitle: Vuoi che IO inoltri un’anteprima dei messaggi all’indirizzo + switch: + title: Invia un’anteprima via email + subtitle: Riceverai un'anteprima del messaggio al tuo indirizzo email, se il servizio lo consente. services: optIn: preferences: diff --git a/ts/navigation/ProfileNavigator.tsx b/ts/navigation/ProfileNavigator.tsx index 77728dabf43..7e796d1efb6 100644 --- a/ts/navigation/ProfileNavigator.tsx +++ b/ts/navigation/ProfileNavigator.tsx @@ -62,9 +62,6 @@ const ProfileStackNavigator = () => ( component={ServicesPreferenceScreen} /> diff --git a/ts/screens/profile/EmailForwardingScreen.tsx b/ts/screens/profile/EmailForwardingScreen.tsx index 16c795bb2a7..a29b5b5d27b 100644 --- a/ts/screens/profile/EmailForwardingScreen.tsx +++ b/ts/screens/profile/EmailForwardingScreen.tsx @@ -1,266 +1,156 @@ /** * A screens to express the preferences related to email forwarding. - * //TODO: magage errors (check toast etc.) + avoid useless updates */ -import { ContentWrapper, IOToast, VSpacer } from "@pagopa/io-app-design-system"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import * as pot from "@pagopa/ts-commons/lib/pot"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import * as React from "react"; -import { View } from "react-native"; -import { connect } from "react-redux"; -import { Dispatch } from "redux"; -import { useNavigation } from "@react-navigation/native"; -import { Body } from "../../components/core/typography/Body"; -import { IOStyles } from "../../components/core/variables/IOStyles"; -import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; -import { EdgeBorderComponent } from "../../components/screens/EdgeBorderComponent"; -import ListItemComponent from "../../components/screens/ListItemComponent"; -import ScreenContent from "../../components/screens/ScreenContent"; -import TopScreenComponent from "../../components/screens/TopScreenComponent"; +import { + ContentWrapper, + ListItemSwitch, + useIOToast +} from "@pagopa/io-app-design-system"; +import _ from "lodash"; +import { IOScrollViewWithLargeHeader } from "../../components/ui/IOScrollViewWithLargeHeader"; import I18n from "../../i18n"; -import { IOStackNavigationProp } from "../../navigation/params/AppParamsList"; -import { ProfileParamsList } from "../../navigation/params/ProfileParamsList"; -import { customEmailChannelSetEnabled } from "../../store/actions/persistedPreferences"; -import { profileUpsert } from "../../store/actions/profile"; +import { + BodyProps, + ComposedBodyFromArray +} from "../../components/core/typography/ComposedBodyFromArray"; +import { useIODispatch, useIOSelector } from "../../store/hooks"; import { isEmailEnabledSelector, profileEmailSelector, profileSelector } from "../../store/reducers/profile"; -import { GlobalState } from "../../store/reducers/types"; -import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; +import { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; +import { profileUpsert } from "../../store/actions/profile"; +import { customEmailChannelSetEnabled } from "../../store/actions/persistedPreferences"; +import { usePrevious } from "../../utils/hooks/usePrevious"; -type OwnProps = { - navigation: IOStackNavigationProp< - ProfileParamsList, - "PROFILE_PREFERENCES_EMAIL_FORWARDING" - >; +const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { + title: "profile.preferences.email.forward.contextualHelpTitle", + body: "profile.preferences.email.forward.contextualHelpContent" }; -type State = { - isCustomChannelEnabledChoice?: boolean; - isLoading: boolean; -}; +const EmailForwardingScreen = () => { + const [isLoading, setIsLoading] = useState(false); + const isCustomChannelEnabledChoice = useRef(undefined); + const IOToast = useIOToast(); + const dispatch = useIODispatch(); + const profile = useIOSelector(profileSelector, _.isEqual); + const prevProfile = usePrevious(profile); + const isEmailEnabled = useIOSelector(isEmailEnabledSelector, _.isEqual); + const UserEmailSelector = useIOSelector(profileEmailSelector, _.isEqual); + const userEmail = pipe( + UserEmailSelector, + O.fold( + () => I18n.t("global.remoteStates.notAvailable"), + (i: string) => i + ) + ); -type Props = OwnProps & - ReturnType & - ReturnType; + const setEmailChannel = useCallback( + (isEmailEnabled: boolean) => { + dispatch( + profileUpsert.request({ + is_email_enabled: isEmailEnabled + }) + ); + }, + [dispatch] + ); -function renderListItem( - title: string, - subTitle: string, - isActive: boolean, - onPress: () => void -) { - return ( - + const setCustomEmailChannelEnabled = useCallback( + (customEmailChannelEnabled: boolean) => { + dispatch(customEmailChannelSetEnabled(customEmailChannelEnabled)); + }, + [dispatch] ); -} -const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { - title: "profile.preferences.email.forward.contextualHelpTitle", - body: "profile.preferences.email.forward.contextualHelpContent" -}; + const disableOrEnableAllEmailNotifications = useCallback( + () => + dispatch( + profileUpsert.request({ + blocked_inbox_or_channels: {}, + is_email_enabled: true + }) + ), + [dispatch] + ); -class EmailForwardingScreenClass extends React.Component { - constructor(props: Props) { - super(props); - this.state = { isLoading: false, isCustomChannelEnabledChoice: undefined }; - } + const bodyPropsArray: Array = [ + { + text: I18n.t("send_email_messages.subtitle") + }, + { + text: <> {userEmail}, + weight: "Semibold" + }, + { + text: I18n.t("global.symbols.question") + } + ]; + + const handleSwitchValueChange = useCallback( + (canSendEmail: boolean) => { + setIsLoading(true); + // eslint-disable-next-line functional/immutable-data + isCustomChannelEnabledChoice.current = false; + if (canSendEmail) { + disableOrEnableAllEmailNotifications(); + } else { + setEmailChannel(false); + } + }, + [disableOrEnableAllEmailNotifications, setEmailChannel] + ); - public componentDidUpdate(prevProps: Props) { - if (pot.isUpdating(prevProps.potProfile)) { + useEffect(() => { + if (prevProfile && pot.isUpdating(prevProfile)) { // if we got an error while updating the preference // show a toast - if (pot.isError(this.props.potProfile)) { + if (pot.isError(profile)) { IOToast.error(I18n.t("global.genericError")); - this.setState({ isLoading: false }); + setIsLoading(false); return; } // TODO move this login into a dedicated saga https://www.pivotaltracker.com/story/show/171600688 // if the profile updating is successfully then apply isCustomChannelEnabledChoice if ( - pot.isSome(this.props.potProfile) && - this.state.isCustomChannelEnabledChoice !== undefined + pot.isSome(profile) && + isCustomChannelEnabledChoice.current !== undefined ) { - this.props.setCustomEmailChannelEnabled( - this.state.isCustomChannelEnabledChoice - ); - this.setState({ isLoading: false }); + setCustomEmailChannelEnabled(isCustomChannelEnabledChoice.current); + setIsLoading(false); + return; } } - } + }, [IOToast, profile, prevProfile, setCustomEmailChannelEnabled]); - public render() { - return ( - - this.props.navigation.goBack()} - > - - - - {I18n.t("send_email_messages.subtitle")} - {` ${this.props.userEmail}`} - {I18n.t("global.symbols.question")} - - - - - {/* ALL INACTIVE */} - {renderListItem( - I18n.t("send_email_messages.options.disable_all.label"), - I18n.t("send_email_messages.options.disable_all.info"), - !this.props.isEmailEnabled, - () => { - if (this.state.isLoading) { - return; - } - // Disable custom email notification and disable email notifications from all visible service - // The upsert of blocked_inbox_or_channels is avoided: the backend will block any email notification - // when is_email_enabled is false - this.setState( - { isCustomChannelEnabledChoice: false, isLoading: true }, - () => { - this.props.setEmailChannel(false); - } - ); - } - )} - {/* ALL ACTIVE */} - {renderListItem( - I18n.t("send_email_messages.options.enable_all.label"), - I18n.t("send_email_messages.options.enable_all.info"), - this.props.isEmailEnabled && - !this.props.isCustomEmailChannelEnabled, - () => { - if (this.state.isLoading) { - return; - } - // Disable custom email notification and enable email notifications from all visible services. - // The upsert of blocked_inbox_or_channels is required to enable those channel that was disabled - // from the service detail - this.setState( - { isCustomChannelEnabledChoice: false, isLoading: true }, - () => { - this.props.disableOrEnableAllEmailNotifications(); - } - ); - } - )} - {/* CASE BY CASE */} - {/* ByService option is disabled until it will be persisted on backend as a proper attribute */} - { - // TODO this option should be reintegrated once option will supported back from backend https://pagopa.atlassian.net/browse/IARS-17 - // renderListItem( - // I18n.t("send_email_messages.options.by_service.label"), - // I18n.t("send_email_messages.options.by_service.info"), - // this.props.isEmailEnabled && - // this.props.isCustomEmailChannelEnabled, - // // Enable custom set of the email notification for each visible service - // () => { - // if (this.state.isLoading) { - // return; - // } - // this.setState( - // { isCustomChannelEnabledChoice: true, isLoading: true }, - // () => { - // this.props.setEmailChannel(true); - // } - // ); - // } - // ) - } - - - - - - - ); - } -} - -const mapStateToProps = (state: GlobalState) => { - const potProfile = profileSelector(state); - // const potIsCustomEmailChannelEnabled = isCustomEmailChannelEnabledSelector( - // state - // ); - // TODO this option should be reintegrated once option will supported back from backend https://pagopa.atlassian.net/browse/IARS-17 - const isCustomEmailChannelEnabled = false; - // : pot.getOrElse(potIsCustomEmailChannelEnabled, false); - - const potUserEmail = profileEmailSelector(state); - const userEmail = pipe( - potUserEmail, - O.fold( - () => I18n.t("global.remoteStates.notAvailable"), - (i: string) => i - ) + return ( + + ) as unknown as string + } + headerActionsProp={{ showHelp: true }} + contextualHelpMarkdown={contextualHelpMarkdown} + canGoback={true} + > + + + + ); - - return { - potProfile, - isLoading: pot.isLoading(potProfile) || pot.isUpdating(potProfile), - isEmailEnabled: isEmailEnabledSelector(state), - isCustomEmailChannelEnabled, - userEmail - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - disableOrEnableAllEmailNotifications: () => - dispatch( - profileUpsert.request({ - blocked_inbox_or_channels: {}, - is_email_enabled: true - }) - ), - setCustomEmailChannelEnabled: (customEmailChannelEnabled: boolean) => { - dispatch(customEmailChannelSetEnabled(customEmailChannelEnabled)); - }, - setEmailChannel: (isEmailEnabled: boolean) => { - dispatch( - profileUpsert.request({ - is_email_enabled: isEmailEnabled - }) - ); - } -}); - -const EmailForwardingScreen = connect( - mapStateToProps, - mapDispatchToProps -)(EmailForwardingScreenClass); - -const EmailForwardingScreenFC = () => { - const navigation = - useNavigation< - IOStackNavigationProp< - ProfileParamsList, - "PROFILE_PREFERENCES_EMAIL_FORWARDING" - > - >(); - - return ; }; -export default EmailForwardingScreenFC; +export default EmailForwardingScreen; From e2e59434b88184e004a1478a766f5b444be19dc1 Mon Sep 17 00:00:00 2001 From: Alessandro Dell'Oste Date: Thu, 4 Jul 2024 15:25:06 +0200 Subject: [PATCH 6/9] wip --- .../cgn/screens/merchants/CgnMerchantsTabsScreen.tsx | 2 +- ts/sagas/startup.ts | 2 +- ts/store/actions/navigation.ts | 10 ---------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsTabsScreen.tsx b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsTabsScreen.tsx index 3d02f4791f5..b89b01dfc97 100644 --- a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsTabsScreen.tsx +++ b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsTabsScreen.tsx @@ -82,7 +82,7 @@ const CgnMerchantsTabsScreen: React.FunctionComponent = (_: Props) => { isSearchAvailable={{ enabled: true, onSearchTap: openFiltersModal }} > - {/* TABS component should be replaced with MaterialTopTab Navigator as done in Services and Messages home */} + {/* TABS component should be replaced with MaterialTopTab Navigator as done in Messages home */} {/* }) ); -/** - * @deprecated - */ -export const navigateToServicePreferenceScreen = () => - NavigationService.dispatchNavigationAction( - CommonActions.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.PROFILE_PREFERENCES_SERVICES - }) - ); - /** * @deprecated */ From 3d31fae771b670c5eefbf7cff3be4e2f4480fe1b Mon Sep 17 00:00:00 2001 From: Alessandro Dell'Oste Date: Thu, 4 Jul 2024 16:01:19 +0200 Subject: [PATCH 7/9] wip --- img/services/icon-loading-services.png | Bin 7458 -> 0 bytes img/services/icon-loading-services@2x.png | Bin 15784 -> 0 bytes img/services/icon-loading-services@3x.png | Bin 24954 -> 0 bytes .../design-system/core/DSLegacyPictograms.tsx | 6 ------ 4 files changed, 6 deletions(-) delete mode 100644 img/services/icon-loading-services.png delete mode 100644 img/services/icon-loading-services@2x.png delete mode 100644 img/services/icon-loading-services@3x.png diff --git a/img/services/icon-loading-services.png b/img/services/icon-loading-services.png deleted file mode 100644 index b0f6e89ba41f24f02118cc001e2582afed0d37f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7458 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 diff --git a/img/services/icon-loading-services@2x.png b/img/services/icon-loading-services@2x.png deleted file mode 100644 index 32bdeb58d249522e42bd34d85bff675a47a6fd96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15784 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 { name={"Search"} image={renderRasterImage(Search)} /> - Date: Thu, 4 Jul 2024 18:24:24 +0200 Subject: [PATCH 8/9] remove `TabNavigation` component --- ts/components/ui/TabNavigation.tsx | 114 ----------------------------- 1 file changed, 114 deletions(-) delete mode 100644 ts/components/ui/TabNavigation.tsx diff --git a/ts/components/ui/TabNavigation.tsx b/ts/components/ui/TabNavigation.tsx deleted file mode 100644 index 0b5b546ab1f..00000000000 --- a/ts/components/ui/TabNavigation.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from "react"; -import { FlexStyle, LayoutChangeEvent, StyleSheet, View } from "react-native"; -import { ScrollView } from "react-native-gesture-handler"; -import { TabItem } from "./TabItem"; - -export type TabNavigationItem = Omit< - TabItem, - "onPress" | "color" | "selected" | "accessibilityLabel" | "accessibilityHint" ->; - -type TabNavigationChildren = - | React.ReactElement - | Array>; - -type TabAlignment = "start" | "center" | "end" | "stretch"; - -type TabNavigation = { - // Configuration - color?: TabItem["color"]; - selectedIndex?: number; - tabAlignment?: TabAlignment; - // Events - onItemPress?: (index: number) => void; - // Tabs - children: TabNavigationChildren; -}; - -const itemsJustify: Record = { - start: "flex-start", - center: "center", - end: "flex-end", - stretch: "space-between" -}; - -const TabNavigation = ({ - color = "light", - selectedIndex: forceSelectedIndex, - tabAlignment = "center", - onItemPress, - children -}: TabNavigation) => { - const [itemMinWidth, setItemMinWidth] = React.useState(0); - const [selectedIndex, setSelectedIndex] = React.useState( - forceSelectedIndex ?? 0 - ); - - const handleItemPress = (index: number) => { - setSelectedIndex(forceSelectedIndex ?? index); - onItemPress?.(index); - }; - - const handleItemOnLayout = (event: LayoutChangeEvent) => { - const { width } = event.nativeEvent.layout; - setItemMinWidth(current => Math.max(current, width)); - }; - - const stretchItems = tabAlignment === "stretch"; - - const wrapChild = (child: React.ReactElement, index: number = 0) => ( - - {React.cloneElement(child, { - onPress: event => { - child.props.onPress?.(event); - handleItemPress(index); - }, - selected: selectedIndex === index, - color - })} - - ); - - return ( - - {Array.isArray(children) ? children.map(wrapChild) : wrapChild(children)} - - ); -}; - -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - paddingHorizontal: 24 - }, - item: { - flexGrow: 0, - flexShrink: 1, - flexBasis: 100, - alignItems: "center" - } -}); - -export { TabNavigation }; From 36cb275ae91ec682be089b3a26536dc5c51d86cf Mon Sep 17 00:00:00 2001 From: Alessandro Dell'Oste Date: Thu, 4 Jul 2024 18:29:30 +0200 Subject: [PATCH 9/9] removed services related components --- .../screens/SectionHeaderComponent.tsx | 86 ------------------- ts/components/services/OrganizationLogo.tsx | 35 -------- 2 files changed, 121 deletions(-) delete mode 100644 ts/components/screens/SectionHeaderComponent.tsx delete mode 100644 ts/components/services/OrganizationLogo.tsx diff --git a/ts/components/screens/SectionHeaderComponent.tsx b/ts/components/screens/SectionHeaderComponent.tsx deleted file mode 100644 index 672d8a10e2a..00000000000 --- a/ts/components/screens/SectionHeaderComponent.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/** - * A component to render a custom section header - * TODO: use the same component for: - * - message list https://www.pivotaltracker.com/story/show/165716236 - */ -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; -import * as React from "react"; -import { - View, - AccessibilityRole, - StyleProp, - StyleSheet, - ViewStyle -} from "react-native"; -import { IOColors, VSpacer } from "@pagopa/io-app-design-system"; -import customVariables from "../../theme/variables"; -import { H2 } from "../core/typography/H2"; -import { IOStyles } from "../core/variables/IOStyles"; -import OrganizationLogo from "../services/OrganizationLogo"; -import { MultiImage } from "../ui/MultiImage"; - -type Props = Readonly<{ - sectionHeader: string; - style?: StyleProp; - logoUri?: React.ComponentProps["source"]; - rightItem?: React.ReactNode; - accessibilityRole?: AccessibilityRole; -}>; - -const styles = StyleSheet.create({ - withoutLogo: { - paddingTop: 19, - paddingBottom: 11, - alignItems: "center" - }, - withLogo: { - paddingTop: customVariables.spacerWidth, - paddingBottom: 0, - alignItems: "center" - }, - sectionView: { - backgroundColor: IOColors.white, - flexDirection: "row", - borderBottomColor: customVariables.itemSeparator, - borderBottomWidth: StyleSheet.hairlineWidth - } -}); - -export default class SectionHeaderComponent extends React.Component { - public render() { - const rightItem = pipe( - this.props.rightItem, - O.fromNullable, - O.getOrElseW(() => - pipe( - this.props.logoUri, - O.fromNullable, - O.map(uri => ), - O.toUndefined - ) - ) - ); - return ( - -

- {this.props.sectionHeader} -

- <> - {rightItem} - - -
- ); - } -} diff --git a/ts/components/services/OrganizationLogo.tsx b/ts/components/services/OrganizationLogo.tsx deleted file mode 100644 index 3a8850ef60c..00000000000 --- a/ts/components/services/OrganizationLogo.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// A component to provide organization logo -import * as React from "react"; -import { ImageStyle, StyleProp, StyleSheet } from "react-native"; -import variables from "../../theme/variables"; -import { MultiImage } from "../ui/MultiImage"; - -type Props = { - logoUri: React.ComponentProps["source"]; - imageStyle?: StyleProp; -}; - -const styles = StyleSheet.create({ - organizationLogo: { - height: 32, - width: 32, - resizeMode: "contain", - marginBottom: 6, - alignSelf: "flex-start", - marginRight: variables.spacingBase - } -}); - -class OrganizationLogo extends React.Component { - public render(): React.ReactNode { - const { logoUri } = this.props; - return ( - - ); - } -} - -export default OrganizationLogo;