From 2e909819b4586b555d0555e239440062c442cd32 Mon Sep 17 00:00:00 2001 From: Alessandro Izzo <34343582+Hantex9@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:23:36 +0200 Subject: [PATCH] chore: [IOBP-236] Banner pay with CIE into IDPay discount initiative (#5005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ⚠️ This PR depends on #4983 and #4995 ## Short description This PR adds a `Banner` inside a discount initiative details if the user has already a CIE onboarded to his account. The banner is shown from the first time the user lands in the bonus detail, and remains visible until: 1. The user closes it via "X" close button 2. The user enables the initiative (either via banner or via toggle within the relevant section) ## List of changes proposed in this pull request - Added a new action preferences to dismiss the banner, the state is persisted on the device based on the initiativeId; - Added a new component `IdPayCodeCieBanner` where is handled all the business logic to show or not the banner ## How to test Open a discount initiative, the first time you open the detail you should see the banner. If you dismiss it, even restarting the app you should not see it anymore. ## Preview https://github.com/pagopa/io-app/assets/34343582/01245e66-b80f-4f72-94f9-5a7b87b37c1c --------- Co-authored-by: forrest57 Co-authored-by: Federico Mastrini --- locales/en/index.yml | 5 ++ locales/it/index.yml | 5 ++ .../code/components/IdPayCodeCieBanner.tsx | 83 +++++++++++++++++++ ts/features/idpay/code/store/actions/index.ts | 12 ++- .../idpay/code/store/reducers/index.ts | 31 ++++++- .../idpay/code/store/selectors/index.ts | 24 ++++++ .../idpay/common/store/reducers/index.ts | 8 +- .../screens/InitiativeDetailsScreen.tsx | 23 +++-- .../InitiativeDetailsScreen.test.tsx | 7 ++ 9 files changed, 183 insertions(+), 15 deletions(-) create mode 100644 ts/features/idpay/code/components/IdPayCodeCieBanner.tsx diff --git a/locales/en/index.yml b/locales/en/index.yml index 5511ee9027e..bc819aef508 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3476,6 +3476,11 @@ idpay: discountDetails: authorizeButton: Autorizza un pagamento IDPayCode: + banner: + title: Paga con la tua carta d’identità elettronica + body: Spendi il denaro a te riservato con la semplicità del contactless. + action: Abilita ora + close: Chiudi failureScreen: cta: Chiudi header: diff --git a/locales/it/index.yml b/locales/it/index.yml index 2f3899c8371..3c9e4750ce4 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3500,6 +3500,11 @@ idpay: discountDetails: authorizeButton: Autorizza un pagamento IDPayCode: + banner: + title: Paga con la tua carta d’identità elettronica + body: Spendi il denaro a te riservato con la semplicità del contactless. + action: Abilita ora + close: Chiudi failureScreen: cta: Chiudi header: diff --git a/ts/features/idpay/code/components/IdPayCodeCieBanner.tsx b/ts/features/idpay/code/components/IdPayCodeCieBanner.tsx new file mode 100644 index 00000000000..21089e1edce --- /dev/null +++ b/ts/features/idpay/code/components/IdPayCodeCieBanner.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; +import { VSpacer, Banner } from "@pagopa/io-app-design-system"; +import { useNavigation } from "@react-navigation/native"; + +import I18n from "../../../../i18n"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { idpayInitiativeInstrumentsGet } from "../../configuration/store/actions"; +import { showIdPayCodeBannerSelector } from "../store/selectors"; +import { IdPayCodeParamsList } from "../navigation/params"; +import { IdPayCodeRoutes } from "../navigation/routes"; +import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; +import { isLoadingDiscountInitiativeInstrumentsSelector } from "../../configuration/store"; +import { idPayCodeCieBannerClose } from "../store/actions"; + +export type IdPayCodeCIEBannerParams = { + initiativeId: string; +}; + +const IdPayCodeCieBanner = ({ initiativeId }: IdPayCodeCIEBannerParams) => { + const bannerViewRef = React.useRef(null); + const navigation = + useNavigation>(); + const dispatch = useIODispatch(); + const showBanner = useIOSelector(showIdPayCodeBannerSelector); + const isLoadingInitiativeInstruments = useIOSelector( + isLoadingDiscountInitiativeInstrumentsSelector + ); + + React.useEffect(() => { + if (initiativeId) { + dispatch( + idpayInitiativeInstrumentsGet.request({ + initiativeId + }) + ); + } + }, [initiativeId, dispatch]); + + const handleOnCloseBanner = () => { + dispatch(idPayCodeCieBannerClose({ initiativeId })); + }; + + const handleNavigateToOnboardingStart = () => { + navigation.navigate(IdPayCodeRoutes.IDPAY_CODE_MAIN, { + screen: IdPayCodeRoutes.IDPAY_CODE_ONBOARDING, + params: { + initiativeId + } + }); + }; + + if (showBanner && !isLoadingInitiativeInstruments) { + return ( + <> + + + + ); + } + + return <>; +}; + +export { IdPayCodeCieBanner }; diff --git a/ts/features/idpay/code/store/actions/index.ts b/ts/features/idpay/code/store/actions/index.ts index c7ceb0027d0..1006951c70f 100644 --- a/ts/features/idpay/code/store/actions/index.ts +++ b/ts/features/idpay/code/store/actions/index.ts @@ -48,8 +48,18 @@ export const idPayEnrollCode = createAsyncAction( export const idPayResetCode = createStandardAction("IDPAY_RESET_CODE")(); +export type IdPayCodeCieBannerClosePayloadType = { + initiativeId: string; +}; + +/** This action is used to close a CIE banner into an initiative details */ +export const idPayCodeCieBannerClose = createStandardAction( + "IDPAY_CODE_CIE_BANNER_CLOSE" +)(); + export type IdPayCodeActions = | ActionType | ActionType | ActionType - | ActionType; + | ActionType + | ActionType; diff --git a/ts/features/idpay/code/store/reducers/index.ts b/ts/features/idpay/code/store/reducers/index.ts index a8429e9f93d..406739be602 100644 --- a/ts/features/idpay/code/store/reducers/index.ts +++ b/ts/features/idpay/code/store/reducers/index.ts @@ -1,8 +1,12 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { getType } from "typesafe-actions"; +import { PersistConfig, persistReducer } from "redux-persist"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + import { Action } from "../../../../../store/actions/types"; import { NetworkError } from "../../../../../utils/errors"; import { + idPayCodeCieBannerClose, idPayEnrollCode, idPayGenerateCode, idPayGetCodeStatus, @@ -13,12 +17,14 @@ export type IdPayCodeState = { isOnboarded: pot.Pot; code: pot.Pot; enrollmentRequest: pot.Pot; + isIdPayInitiativeBannerClosed: Record; }; const INITIAL_STATE: IdPayCodeState = { isOnboarded: pot.none, code: pot.none, - enrollmentRequest: pot.none + enrollmentRequest: pot.none, + isIdPayInitiativeBannerClosed: {} }; const reducer = ( @@ -92,8 +98,29 @@ const reducer = ( ...state, code: pot.none }; + case getType(idPayCodeCieBannerClose): + return { + ...state, + isIdPayInitiativeBannerClosed: { + [action.payload.initiativeId]: true + } + }; } return state; }; -export default reducer; +const CURRENT_REDUX_FEATURES_STORE_VERSION = -1; + +const persistConfig: PersistConfig = { + key: "code", + storage: AsyncStorage, + version: CURRENT_REDUX_FEATURES_STORE_VERSION, + whitelist: ["isIdPayInitiativeBannerClosed"] +}; + +export const idPayCodePersistor = persistReducer( + persistConfig, + reducer +); + +export default idPayCodePersistor; diff --git a/ts/features/idpay/code/store/selectors/index.ts b/ts/features/idpay/code/store/selectors/index.ts index 8319dbc7a5a..8f460fde1b2 100644 --- a/ts/features/idpay/code/store/selectors/index.ts +++ b/ts/features/idpay/code/store/selectors/index.ts @@ -2,6 +2,9 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { createSelector } from "reselect"; import { GlobalState } from "../../../../../store/reducers/types"; import { IdPayCodeState } from "../reducers"; +import { idpayDiscountInitiativeInstrumentsSelector } from "../../../configuration/store"; +import { InstrumentTypeEnum } from "../../../../../../definitions/idpay/InstrumentDTO"; +import { idpayInitiativeIdSelector } from "../../../details/store"; export const idPayCodeStateSelector = (state: GlobalState): IdPayCodeState => state.features.idPay.code; @@ -20,3 +23,24 @@ export const idPayCodeSelector = createSelector( idPayCodeStateSelector, state => state.code ); + +export const isIdPayInitiativeBannerClosedSelector = (state: GlobalState) => + state.features.idPay.code.isIdPayInitiativeBannerClosed; + +export const hasIdPayCodeInstrument = createSelector( + idpayDiscountInitiativeInstrumentsSelector, + instruments => + instruments.some( + instrument => instrument.instrumentType === InstrumentTypeEnum.IDPAYCODE + ) +); + +export const showIdPayCodeBannerSelector = createSelector( + idpayInitiativeIdSelector, + isIdPayInitiativeBannerClosedSelector, + hasIdPayCodeInstrument, + (initiativeId, initiativeBannerClosed, hasIdPayCodeInstrument) => + initiativeId !== undefined && + (!initiativeBannerClosed || !initiativeBannerClosed[initiativeId]) && + !hasIdPayCodeInstrument +); diff --git a/ts/features/idpay/common/store/reducers/index.ts b/ts/features/idpay/common/store/reducers/index.ts index 9ab75cf8994..aeb3325509f 100644 --- a/ts/features/idpay/common/store/reducers/index.ts +++ b/ts/features/idpay/common/store/reducers/index.ts @@ -1,5 +1,7 @@ import { combineReducers } from "redux"; -import codeReducer, { IdPayCodeState } from "../../../code/store/reducers"; +import { PersistPartial } from "redux-persist"; + +import codePersistor, { IdPayCodeState } from "../../../code/store/reducers"; import initiativeDetailsReducer, { IdPayInitiativeState } from "../../../details/store/index"; @@ -16,14 +18,14 @@ export type IDPayState = { initiative: IdPayInitiativeState; timeline: IdPayTimelineState; configuration: IdPayInitiativeConfigurationState; - code: IdPayCodeState; + code: IdPayCodeState & PersistPartial; }; const idPayReducer = combineReducers({ wallet: walletReducer, initiative: initiativeDetailsReducer, timeline: timelineReducer, - code: codeReducer, + code: codePersistor, configuration: configurationReducer }); diff --git a/ts/features/idpay/details/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/details/screens/InitiativeDetailsScreen.tsx index 205d1df7211..c323f4e286e 100644 --- a/ts/features/idpay/details/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/details/screens/InitiativeDetailsScreen.tsx @@ -12,6 +12,7 @@ import { Pictogram, ContentWrapper } from "@pagopa/io-app-design-system"; +import Animated, { Layout } from "react-native-reanimated"; import { InitiativeDTO, InitiativeRewardTypeEnum, @@ -42,6 +43,7 @@ import { idpayInitiativeGet, idpayTimelinePageGet } from "../store/actions"; import { IDPayPaymentRoutes } from "../../payment/navigation/navigator"; import { InitiativeDiscountSettingsComponent } from "../components/InitiativeDiscountSettingsComponent"; import { IDPayConfigurationRoutes } from "../../configuration/navigation/navigator"; +import { IdPayCodeCieBanner } from "../../code/components/IdPayCodeCieBanner"; export type InitiativeDetailsScreenParams = { initiativeId: string; @@ -180,15 +182,18 @@ const InitiativeDetailsScreen = () => { return ( - - - - + + + + + + + ); diff --git a/ts/features/idpay/details/screens/__tests__/InitiativeDetailsScreen.test.tsx b/ts/features/idpay/details/screens/__tests__/InitiativeDetailsScreen.test.tsx index 3047524cce3..c959a50c3f2 100644 --- a/ts/features/idpay/details/screens/__tests__/InitiativeDetailsScreen.test.tsx +++ b/ts/features/idpay/details/screens/__tests__/InitiativeDetailsScreen.test.tsx @@ -16,6 +16,13 @@ import { NetworkError } from "../../../../../utils/errors"; import I18n from "../../../../../i18n"; import { formatDateAsLocal } from "../../../../../utils/dates"; +jest.mock("react-native-reanimated", () => ({ + ...require("react-native-reanimated/mock"), + Layout: { + duration: jest.fn() + } +})); + const mockedInitiative: InitiativeDTO = { endDate: new Date(2023, 1, 1), initiativeId: "ABC123",