diff --git a/img/features/itWallet/discovery/itw_hero.png b/img/features/itWallet/discovery/itw_hero.png index 57e1e321b3e..895d9b909a9 100644 Binary files a/img/features/itWallet/discovery/itw_hero.png and b/img/features/itWallet/discovery/itw_hero.png differ diff --git a/img/features/itWallet/discovery/itw_hero@2x.png b/img/features/itWallet/discovery/itw_hero@2x.png index c346963de82..ceecb060e90 100644 Binary files a/img/features/itWallet/discovery/itw_hero@2x.png and b/img/features/itWallet/discovery/itw_hero@2x.png differ diff --git a/img/features/itWallet/discovery/itw_hero@3x.png b/img/features/itWallet/discovery/itw_hero@3x.png index af5b39acb1c..453d23f4dd1 100644 Binary files a/img/features/itWallet/discovery/itw_hero@3x.png and b/img/features/itWallet/discovery/itw_hero@3x.png differ diff --git a/locales/en/index.yml b/locales/en/index.yml index 5cc6ea266a8..6ee8d80dae2 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -4189,6 +4189,13 @@ fastLogin: whatsNew: title: Logging into IO just got easier! subtitle: From now on, to log into the app you'll have to use your SPID or CIE only once a year. After that you can just use your unlock code, fingerprint or face. +loginFeatures: + loginPreferences: + expirationBanner: + title: Quick access is about to expire. + content: On {{date}} you will need to log in again with SPID or CIE + action: + label: Read more whatsNew: title: What's changed? bonusCard: diff --git a/locales/it/index.yml b/locales/it/index.yml index f89e08b3eab..ec978aff83b 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3321,7 +3321,7 @@ features: content: Attiva Documenti su IO per aggiungere al Portafoglio la versione digitale dei tuoi documenti! action: Inizia title: La versione digitale dei tuoi documenti, su IO - content: "###### Documenti su IO: come funziona \n Da oggi puoi aggiungere al Portafoglio di IO **la versione digitale dei tuoi documenti** personali, come la Patente di guida e la Tessera Sanitaria. \n\n Attiva la funzionalità **Documenti su IO** per averli sempre a portata di mano sul tuo dispositivo. \n\n ###### È facile e veloce \n Avrai bisogno delle credenziali **SPID** o **CIE** (Carta d’Identità Elettronica) per completare l’attivazione: è un passaggio di sicurezza necessario per garantire la sicurezza dei tuoi dati." + content: "Aggiungi la versione digitale dei tuoi documenti personali al Portafoglio dell'app! \n\n Hanno lo **stesso valore legale** di quelli fisici, in specifici contesti d'uso. \n\n ###### È facile e veloce \n Continua per attivare la nuova funzionalità in meno di 3 minuti: oltre **4 Milioni** di persone stanno già usando i **Documenti su IO**!" tos: Premendo **Continua** dichiari di aver letto e compreso l’[Informativa Privacy e i Termini e Condizioni d’uso]({{privacyAndTosUrl}}). upcomingWalletBanner: title: "Novità in arrivo: i tuoi Documenti su IO" @@ -4189,6 +4189,13 @@ fastLogin: whatsNew: title: Abbiamo migliorato l’accesso a IO! subtitle: Ora puoi accedere all'app utilizzando il tuo SPID o la tua CIE solo una volta all'anno. Ti basterà poi utilizzare il tuo codice di sblocco o il biometrico per entrare. +loginFeatures: + loginPreferences: + expirationBanner: + title: Accesso rapido in scadenza + content: Il {{date}} dovrai accedere di nuovo con SPID o CIE + action: + label: Leggi come fare whatsNew: title: Cosa cambia? bonusCard: diff --git a/scripts/generate-api-models.sh b/scripts/generate-api-models.sh index 85309a5d098..43d8e461f1f 100755 --- a/scripts/generate-api-models.sh +++ b/scripts/generate-api-models.sh @@ -2,7 +2,9 @@ IO_BACKEND_VERSION=v16.7.4-RELEASE # need to change after merge on io-services-metadata -IO_SERVICES_METADATA_VERSION=1.0.57 +IO_SERVICES_METADATA_VERSION=1.0.59 +# Session manager version +IO_SESSION_MANAGER_VERSION=1.4.0 declare -a apis=( # Backend APIs @@ -30,8 +32,8 @@ declare -a apis=( # CDN APIs "./definitions/content https://raw.githubusercontent.com/pagopa/io-services-metadata/$IO_SERVICES_METADATA_VERSION/definitions.yml" # Session Manager APIs - "./definitions/session_manager https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@1.0.0/apps/io-session-manager/api/internal.yaml" - "./definitions/fast_login https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@1.0.0/apps/io-session-manager/api/fast-login.yaml" + "./definitions/session_manager https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@$IO_SESSION_MANAGER_VERSION/apps/io-session-manager/api/internal.yaml" + "./definitions/fast_login https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@$IO_SESSION_MANAGER_VERSION/apps/io-session-manager/api/fast-login.yaml" # CGN APIs "./definitions/cgn https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_cgn.yaml" "./definitions/cgn/merchants https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_cgn_operator_search.yaml" @@ -49,7 +51,7 @@ wait declare -a apisNoClient=( "./definitions/backend https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_public.yaml" - "./definitions/session_manager https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@1.0.0/apps/io-session-manager/api/public.yaml" + "./definitions/session_manager https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@$IO_SESSION_MANAGER_VERSION/apps/io-session-manager/api/public.yaml" "./definitions/pn https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/openapi/consumed/api-piattaforma-notifiche.yaml" ) diff --git a/ts/features/barcode/screens/BarcodeScanScreen.tsx b/ts/features/barcode/screens/BarcodeScanScreen.tsx index a85cb38a671..75556646fa2 100644 --- a/ts/features/barcode/screens/BarcodeScanScreen.tsx +++ b/ts/features/barcode/screens/BarcodeScanScreen.tsx @@ -42,8 +42,8 @@ import { useHardwareBackButton } from "../../../hooks/useHardwareBackButton"; import { usePagoPaPayment } from "../../payments/checkout/hooks/usePagoPaPayment"; import { FCI_ROUTES } from "../../fci/navigation/routes"; import { paymentAnalyticsDataSelector } from "../../payments/history/store/selectors"; -import { ITW_REMOTE_ROUTES } from "../../itwallet/presentation/remote/navigation/routes.ts"; import { isIdPayLocallyEnabledSelector } from "../../../store/reducers/persistedPreferences.ts"; +import { ITW_REMOTE_ROUTES } from "../../itwallet/presentation/remote/navigation/routes.ts"; const BarcodeScanScreen = () => { const navigation = useNavigation>(); @@ -156,10 +156,8 @@ const BarcodeScanScreen = () => { break; case "ITW_REMOTE": navigation.navigate(ITW_REMOTE_ROUTES.MAIN, { - screen: ITW_REMOTE_ROUTES.CLAIMS_DISCLOSURE, - params: { - itwRemoteRequestPayload: barcode.itwRemoteRequestPayload - } + screen: ITW_REMOTE_ROUTES.REQUEST_VALIDATION, + params: barcode.itwRemoteRequestPayload }); break; } diff --git a/ts/features/barcode/types/__tests__/decoders.test.ts b/ts/features/barcode/types/__tests__/decoders.test.ts index e5873f5afda..bf65905d11a 100644 --- a/ts/features/barcode/types/__tests__/decoders.test.ts +++ b/ts/features/barcode/types/__tests__/decoders.test.ts @@ -98,10 +98,10 @@ describe("test decodeIOBarcode function", () => { O.some({ type: "ITW_REMOTE", itwRemoteRequestPayload: { - clientId: "abc123xy", - requestUri: "https://example.com/callback", + client_id: "abc123xy", + request_uri: "https://example.com/callback", state: "hyqizm592", - requestUriMethod: "GET" + request_uri_method: "GET" } }) ); @@ -117,10 +117,10 @@ describe("test decodeIOBarcode function", () => { O.some({ type: "ITW_REMOTE", itwRemoteRequestPayload: { - clientId: "abc123xy", - requestUri: "https://example.com/callback", + client_id: "abc123xy", + request_uri: "https://example.com/callback", state: "hyqizm592", - requestUriMethod: "POST" + request_uri_method: "POST" } }) ); diff --git a/ts/features/barcode/types/decoders.ts b/ts/features/barcode/types/decoders.ts index a3d6aba53d3..81355143b78 100644 --- a/ts/features/barcode/types/decoders.ts +++ b/ts/features/barcode/types/decoders.ts @@ -9,11 +9,9 @@ import * as A from "fp-ts/lib/Array"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { sequenceS } from "fp-ts/lib/Apply"; -import { decodePosteDataMatrix } from "../../../utils/payment"; import { SignatureRequestDetailView } from "../../../../definitions/fci/SignatureRequestDetailView"; +import { decodePosteDataMatrix } from "../../../utils/payment"; import { ItwRemoteRequestPayload } from "../../itwallet/presentation/remote/Utils/itwRemoteTypeUtils.ts"; -import { getUrlParam } from "../../itwallet/common/utils/itwUrlUtils.ts"; import { IOBarcodeType } from "./IOBarcode"; // Discriminated barcode type @@ -119,25 +117,18 @@ const decodeItwRemoteBarcode: IOBarcodeDecoderFn = (data: string) => O.fromNullable( data.match(/^https:\/\/continua\.io\.pagopa\.it\/itw\/auth\?(.*)$/) ), - O.chain(([url]) => - sequenceS(O.Monad)({ - clientId: getUrlParam(url, "client_id"), - requestUri: getUrlParam(url, "request_uri"), - state: getUrlParam(url, "state"), - requestUriMethod: pipe( - getUrlParam(url, "request_uri_method"), - O.alt(() => O.some("GET")) - ) + O.map(match => new URLSearchParams(match[1])), + O.chainEitherK(params => + ItwRemoteRequestPayload.decode({ + client_id: params.get("client_id"), + request_uri: params.get("request_uri"), + state: params.get("state"), + request_uri_method: params.get("request_uri_method") ?? "GET" }) ), - O.map(({ clientId, requestUri, state, requestUriMethod }) => ({ + O.map(itwRemoteRequestPayload => ({ type: "ITW_REMOTE", - itwRemoteRequestPayload: { - clientId, - requestUri, - state, - requestUriMethod - } + itwRemoteRequestPayload })) ); diff --git a/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap b/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap index 84c5861d5b6..0d463bf8844 100644 --- a/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap +++ b/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap @@ -139,6 +139,7 @@ exports[`featuresPersistor should match snapshot 1`] = ` }, "landingBanners": { "ITW_DISCOVERY": true, + "LV_EXPIRATION_REMINDER": true, "PUSH_NOTIFICATIONS_REMINDER": true, "SETTINGS_DISCOVERY": true, }, @@ -169,6 +170,9 @@ exports[`featuresPersistor should match snapshot 1`] = ` "loginInfo": { "userFromSuccessLogin": false, }, + "loginPreferences": { + "showSessionExpirationBanner": true, + }, "nativeLogin": { "enabled": true, }, diff --git a/ts/features/common/store/reducers/index.ts b/ts/features/common/store/reducers/index.ts index d7825f4b76d..d9bf2ba71ba 100644 --- a/ts/features/common/store/reducers/index.ts +++ b/ts/features/common/store/reducers/index.ts @@ -63,6 +63,10 @@ import { import { GlobalState } from "../../../../store/reducers/types"; import { isIOMarkdownDisabledForMessagesAndServices } from "../../../../store/reducers/backendStatus/remoteConfig"; import { isIOMarkdownEnabledLocallySelector } from "../../../../store/reducers/persistedPreferences"; +import { + loginPreferencesPersistor, + LoginPreferencesState +} from "../../../login/preferences/store/reducers"; type LoginFeaturesState = { testLogin: TestLoginState; @@ -71,6 +75,7 @@ type LoginFeaturesState = { cieLogin: CieLoginState & PersistPartial; loginInfo: LoginInfoState; spidLogin: SpidLoginState; + loginPreferences: LoginPreferencesState & PersistPartial; }; export type FeaturesState = { @@ -105,7 +110,8 @@ const rootReducer = combineReducers({ fastLogin: fastLoginReducer, cieLogin: cieLoginPersistor, loginInfo: loginInfoReducer, - spidLogin: spidLoginReducer + spidLogin: spidLoginReducer, + loginPreferences: loginPreferencesPersistor }), wallet: walletReducer, fims: fimsReducer, diff --git a/ts/features/itwallet/discovery/screens/ItwDiscoveryInfoScreen.tsx b/ts/features/itwallet/discovery/screens/ItwDiscoveryInfoScreen.tsx index b313a9c09bb..439b310f507 100644 --- a/ts/features/itwallet/discovery/screens/ItwDiscoveryInfoScreen.tsx +++ b/ts/features/itwallet/discovery/screens/ItwDiscoveryInfoScreen.tsx @@ -1,10 +1,4 @@ -import { - ContentWrapper, - FooterActions, - ForceScrollDownView, - H1, - VSpacer -} from "@pagopa/io-app-design-system"; +import { ContentWrapper, H1, VSpacer } from "@pagopa/io-app-design-system"; import { useFocusEffect } from "@react-navigation/native"; import { StyleSheet } from "react-native"; import { AnimatedImage } from "../../../../components/AnimatedImage"; @@ -24,6 +18,7 @@ import { itwIsActivationDisabledSelector } from "../../common/store/selectors/re import { selectIsLoading } from "../../machine/eid/selectors"; import { ItwEidIssuanceMachineContext } from "../../machine/provider"; import { generateLinkRuleWithCallback } from "../../common/utils/markdown"; +import { IOScrollView } from "../../../../components/ui/IOScrollView.tsx"; /** * This is the screen that shows the information about the discovery process @@ -56,7 +51,19 @@ const ItwDiscoveryInfoScreen = () => { }); return ( - + { rules={generateLinkRuleWithCallback(trackOpenItwTos)} /> - - + ); }; diff --git a/ts/features/itwallet/navigation/useItwLinkingOptions.tsx b/ts/features/itwallet/navigation/useItwLinkingOptions.tsx index 4da52c2ec9a..25be01a2858 100644 --- a/ts/features/itwallet/navigation/useItwLinkingOptions.tsx +++ b/ts/features/itwallet/navigation/useItwLinkingOptions.tsx @@ -3,6 +3,7 @@ import { AppParamsList } from "../../../navigation/params/AppParamsList"; import { useIOSelector } from "../../../store/hooks"; import { isItwEnabledSelector } from "../common/store/selectors/remoteConfig"; import { itwLifecycleIsValidSelector } from "../lifecycle/store/selectors"; +import { ITW_REMOTE_ROUTES } from "../presentation/remote/navigation/routes.ts"; import { ITW_ROUTES } from "./routes"; /** @@ -30,6 +31,14 @@ export const useItwLinkingOptions = (): PathConfigMap => { } }) } + }, + [ITW_REMOTE_ROUTES.MAIN]: { + path: "itw/auth", + screens: { + [ITW_REMOTE_ROUTES.REQUEST_VALIDATION]: { + path: "" + } + } } }; }; diff --git a/ts/features/itwallet/presentation/remote/Utils/itwRemoteTypeUtils.ts b/ts/features/itwallet/presentation/remote/Utils/itwRemoteTypeUtils.ts index ef13b85a75e..ad1fc6f3cca 100644 --- a/ts/features/itwallet/presentation/remote/Utils/itwRemoteTypeUtils.ts +++ b/ts/features/itwallet/presentation/remote/Utils/itwRemoteTypeUtils.ts @@ -1,8 +1,17 @@ +import * as t from "io-ts"; +import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; + // TODO: This will be imported from io-react-native-wallet, when the type will be available // Remote presentation QR code data -export type ItwRemoteRequestPayload = { - clientId: string; - requestUri: string; - state: string; - requestUriMethod?: string; -}; +export const ItwRemoteRequestPayload = t.intersection([ + t.type({ + client_id: NonEmptyString, + request_uri: NonEmptyString, + state: NonEmptyString + }), + t.partial({ + request_uri_method: t.union([t.string, t.null, t.undefined]) + }) +]); + +export type ItwRemoteRequestPayload = t.TypeOf; diff --git a/ts/features/itwallet/presentation/remote/machine/__tests__/machine.test.ts b/ts/features/itwallet/presentation/remote/machine/__tests__/machine.test.ts index 856b678c313..bc8f26527e3 100644 --- a/ts/features/itwallet/presentation/remote/machine/__tests__/machine.test.ts +++ b/ts/features/itwallet/presentation/remote/machine/__tests__/machine.test.ts @@ -1,6 +1,7 @@ import _ from "lodash"; import { StateFrom, createActor } from "xstate"; import { ItwRemoteMachine, itwRemoteMachine } from "../machine.ts"; +import { ItwRemoteRequestPayload } from "../../Utils/itwRemoteTypeUtils.ts"; const T_CLIENT_ID = "clientId"; const T_REQUEST_URI = "https://example.com"; @@ -12,6 +13,7 @@ describe("itwRemoteMachine", () => { const navigateToDiscoveryScreen = jest.fn(); const navigateToWallet = jest.fn(); const navigateToFailureScreen = jest.fn(); + const navigateToClaimsDisclosureScreen = jest.fn(); const closeIssuance = jest.fn(); const isWalletActive = jest.fn(); @@ -21,6 +23,7 @@ describe("itwRemoteMachine", () => { navigateToDiscoveryScreen, navigateToWallet, navigateToFailureScreen, + navigateToClaimsDisclosureScreen, closeIssuance }, actors: {}, @@ -49,10 +52,10 @@ describe("itwRemoteMachine", () => { actor.send({ type: "start", payload: { - clientId: T_CLIENT_ID, - requestUri: T_REQUEST_URI, + client_id: T_CLIENT_ID, + request_uri: T_REQUEST_URI, state: T_STATE - } + } as ItwRemoteRequestPayload }); expect(actor.getSnapshot().value).toStrictEqual("Failure"); @@ -67,10 +70,10 @@ describe("itwRemoteMachine", () => { value: "Failure", context: { payload: { - clientId: T_CLIENT_ID, - requestUri: T_REQUEST_URI, + client_id: T_CLIENT_ID, + request_uri: T_REQUEST_URI, state: T_STATE - } + } as ItwRemoteRequestPayload } } as MachineSnapshot); @@ -91,10 +94,10 @@ describe("itwRemoteMachine", () => { value: "Failure", context: { payload: { - clientId: T_CLIENT_ID, - requestUri: T_REQUEST_URI, + client_id: T_CLIENT_ID, + request_uri: T_REQUEST_URI, state: T_STATE - } + } as ItwRemoteRequestPayload } } as MachineSnapshot); @@ -116,12 +119,13 @@ describe("itwRemoteMachine", () => { actor.send({ type: "start", payload: { - clientId: T_CLIENT_ID, - requestUri: T_REQUEST_URI, + client_id: T_CLIENT_ID, + request_uri: T_REQUEST_URI, state: T_STATE - } + } as ItwRemoteRequestPayload }); + expect(navigateToClaimsDisclosureScreen).toHaveBeenCalledTimes(1); expect(actor.getSnapshot().value).toStrictEqual("ClaimsDisclosure"); }); }); diff --git a/ts/features/itwallet/presentation/remote/machine/actions.ts b/ts/features/itwallet/presentation/remote/machine/actions.ts index b818721d4a1..5bb7540f2cd 100644 --- a/ts/features/itwallet/presentation/remote/machine/actions.ts +++ b/ts/features/itwallet/presentation/remote/machine/actions.ts @@ -32,6 +32,12 @@ export const createRemoteActionsImplementation = ( }); }, + navigateToClaimsDisclosureScreen: () => { + navigation.replace(ITW_REMOTE_ROUTES.MAIN, { + screen: ITW_REMOTE_ROUTES.CLAIMS_DISCLOSURE + }); + }, + closeIssuance: () => { navigation.popToTop(); } diff --git a/ts/features/itwallet/presentation/remote/machine/machine.ts b/ts/features/itwallet/presentation/remote/machine/machine.ts index fa09a6538f1..a56bc15db7d 100644 --- a/ts/features/itwallet/presentation/remote/machine/machine.ts +++ b/ts/features/itwallet/presentation/remote/machine/machine.ts @@ -1,5 +1,4 @@ import { assign, not, setup } from "xstate"; -import { ItwTags } from "../../../machine/tags"; import { InitialContext, Context } from "./context"; import { mapEventToFailure, RemoteFailureType } from "./failure"; import { RemoteEvents } from "./events"; @@ -18,6 +17,7 @@ export const itwRemoteMachine = setup({ navigateToFailureScreen: notImplemented, navigateToDiscoveryScreen: notImplemented, navigateToWallet: notImplemented, + navigateToClaimsDisclosureScreen: notImplemented, closeIssuance: notImplemented }, actors: {}, @@ -38,13 +38,13 @@ export const itwRemoteMachine = setup({ actions: assign(({ event }) => ({ payload: event.payload })), - target: "PayloadValidation" + target: "PreliminaryChecks" } } }, - PayloadValidation: { - description: "Validating the remote request payload before proceeding", - tags: [ItwTags.Loading], + PreliminaryChecks: { + description: + "Perform preliminary checks on the wallet and necessary conditions before proceeding", always: [ { guard: not("isWalletActive"), @@ -62,6 +62,7 @@ export const itwRemoteMachine = setup({ ] }, ClaimsDisclosure: { + entry: "navigateToClaimsDisclosureScreen", description: "Display the list of claims to disclose for the verifiable presentation", on: { diff --git a/ts/features/itwallet/presentation/remote/navigation/ItwRemoteParamsList.ts b/ts/features/itwallet/presentation/remote/navigation/ItwRemoteParamsList.ts index 9e6bcf4b513..53619cd8abb 100644 --- a/ts/features/itwallet/presentation/remote/navigation/ItwRemoteParamsList.ts +++ b/ts/features/itwallet/presentation/remote/navigation/ItwRemoteParamsList.ts @@ -1,7 +1,8 @@ -import { ItwRemoteClaimsDisclosureScreenNavigationParams } from "../screens/ItwRemoteClaimsDisclosureScreen.tsx"; +import { ItwRemoteRequestValidationScreenNavigationParams } from "../screens/ItwRemoteRequestValidationScreen.tsx"; import { ITW_REMOTE_ROUTES } from "./routes.ts"; export type ItwRemoteParamsList = { - [ITW_REMOTE_ROUTES.CLAIMS_DISCLOSURE]: ItwRemoteClaimsDisclosureScreenNavigationParams; + [ITW_REMOTE_ROUTES.REQUEST_VALIDATION]: ItwRemoteRequestValidationScreenNavigationParams; + [ITW_REMOTE_ROUTES.CLAIMS_DISCLOSURE]: undefined; [ITW_REMOTE_ROUTES.FAILURE]: undefined; }; diff --git a/ts/features/itwallet/presentation/remote/navigation/ItwRemoteStackNavigator.tsx b/ts/features/itwallet/presentation/remote/navigation/ItwRemoteStackNavigator.tsx index aa884169b9c..5134f839d46 100644 --- a/ts/features/itwallet/presentation/remote/navigation/ItwRemoteStackNavigator.tsx +++ b/ts/features/itwallet/presentation/remote/navigation/ItwRemoteStackNavigator.tsx @@ -4,7 +4,9 @@ import { ItwRemoteMachineContext, ItwRemoteMachineProvider } from "../machine/provider.tsx"; + import { ItwRemoteClaimsDisclosureScreen } from "../screens/ItwRemoteClaimsDisclosureScreen.tsx"; +import { ItwRemoteRequestValidationScreen } from "../screens/ItwRemoteRequestValidationScreen.tsx"; import { ItwRemoteFailureScreen } from "../screens/ItwRemoteFailureScreen.tsx"; import { ITW_REMOTE_ROUTES } from "./routes.ts"; import { ItwRemoteParamsList } from "./ItwRemoteParamsList.ts"; @@ -24,7 +26,7 @@ const InnerNavigator = () => { return ( { @@ -32,6 +34,11 @@ const InnerNavigator = () => { } }} > + ; - -const QRCodeValidationScreen = () => ( - - - - {I18n.t("features.itWallet.presentation.remote.loadingScreen.subtitle")} - - - -); - -const ItwRemoteClaimsDisclosureScreen = (params: ScreenProps) => { +const ItwRemoteClaimsDisclosureScreen = () => { usePreventScreenCapture(); useItwDisableGestureNavigation(); useAvoidHardwareBackButton(); - const machineRef = ItwRemoteMachineContext.useActorRef(); - - const isMachineLoading = ItwRemoteMachineContext.useSelector(selectIsLoading); - - const itwRemoteRequestPayload = params.route.params.itwRemoteRequestPayload; - - useFocusEffect( - useCallback(() => { - if (itwRemoteRequestPayload) { - machineRef.send({ - type: "start", - payload: itwRemoteRequestPayload - }); - } - }, [itwRemoteRequestPayload, machineRef]) - ); - - const dismissDialog = useItwDismissalDialog(() => { - machineRef.send({ type: "close" }); - }); - - useHeaderSecondLevel({ title: "", goBack: dismissDialog.show }); - - if (isMachineLoading) { - return ; - } - return ; }; diff --git a/ts/features/itwallet/presentation/remote/screens/ItwRemoteRequestValidationScreen.tsx b/ts/features/itwallet/presentation/remote/screens/ItwRemoteRequestValidationScreen.tsx new file mode 100644 index 00000000000..9769201dc32 --- /dev/null +++ b/ts/features/itwallet/presentation/remote/screens/ItwRemoteRequestValidationScreen.tsx @@ -0,0 +1,84 @@ +import { Body, IOStyles } from "@pagopa/io-app-design-system"; +import { useFocusEffect } from "@react-navigation/native"; +import { useCallback } from "react"; +import { View } from "react-native"; +import LoadingScreenContent from "../../../../../components/screens/LoadingScreenContent.tsx"; +import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent.tsx"; +import I18n from "../../../../../i18n.ts"; +import { + IOStackNavigationRouteProps, + useIONavigation +} from "../../../../../navigation/params/AppParamsList.ts"; +import { useItwDisableGestureNavigation } from "../../../common/hooks/useItwDisableGestureNavigation.ts"; +import { ItwRemoteRequestPayload } from "../Utils/itwRemoteTypeUtils.ts"; +import { ItwRemoteMachineContext } from "../machine/provider.tsx"; +import { ItwRemoteParamsList } from "../navigation/ItwRemoteParamsList.ts"; + +export type ItwRemoteRequestValidationScreenNavigationParams = + Partial; + +type ScreenProps = IOStackNavigationRouteProps< + ItwRemoteParamsList, + "ITW_REMOTE_REQUEST_VALIDATION" +>; + +const ItwRemoteRequestValidationScreen = (params: ScreenProps) => { + const navigation = useIONavigation(); + useItwDisableGestureNavigation(); + + // Add default value for request_uri_method if not present + const payload = { + ...params.route.params, + request_uri_method: params.route.params.request_uri_method ?? "GET" + }; + + if (!ItwRemoteRequestPayload.is(payload)) { + // TODO: handle invalid payload failure for deep link [1961] + return ( + navigation.goBack() + }} + /> + ); + } + + return ; +}; + +const ContentView = ({ payload }: { payload: ItwRemoteRequestPayload }) => { + const machineRef = ItwRemoteMachineContext.useActorRef(); + + useFocusEffect( + useCallback(() => { + machineRef.send({ + type: "start", + payload + }); + }, [payload, machineRef]) + ); + + return ( + + + + {I18n.t( + "features.itWallet.presentation.remote.loadingScreen.subtitle" + )} + + + + ); +}; + +export { ItwRemoteRequestValidationScreen }; diff --git a/ts/features/itwallet/presentation/remote/screens/__tests__/ItwRemoteRequestValidation.test.tsx b/ts/features/itwallet/presentation/remote/screens/__tests__/ItwRemoteRequestValidation.test.tsx new file mode 100644 index 00000000000..29e0d74bfa8 --- /dev/null +++ b/ts/features/itwallet/presentation/remote/screens/__tests__/ItwRemoteRequestValidation.test.tsx @@ -0,0 +1,97 @@ +import { createStore } from "redux"; +import { ITW_REMOTE_ROUTES } from "../../navigation/routes.ts"; +import { renderScreenWithNavigationStoreContext } from "../../../../../../utils/testWrapper.tsx"; +import { GlobalState } from "../../../../../../store/reducers/types.ts"; +import { ItwRemoteRequestValidationScreen } from "../ItwRemoteRequestValidationScreen.tsx"; +import { appReducer } from "../../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../../store/actions/application.ts"; +import { ItwRemoteRequestPayload } from "../../Utils/itwRemoteTypeUtils.ts"; +import { ItwRemoteMachineContext } from "../../machine/provider.tsx"; +import { IOStackNavigationProp } from "../../../../../../navigation/params/AppParamsList.ts"; +import { ItwRemoteParamsList } from "../../navigation/ItwRemoteParamsList.ts"; +import { itwRemoteMachine } from "../../machine/machine.ts"; + +describe("ItwRemoteRequestValidationScreen", () => { + it("it should render the screen correctly", () => { + const component = renderComponent(); + expect(component).toBeTruthy(); + }); + + it("should render the loading screen if payload is valid", () => { + const validPayload = { + client_id: "abc123xy", + request_uri: "https://example.com/callback", + state: "hyqizm592" + } as ItwRemoteRequestPayload; + + const { getByTestId } = renderComponent(validPayload); + + expect(getByTestId("loader")).toBeTruthy(); + }); + + it("should render failure screen if missing required fields", () => { + const partialPayload = { + client_id: "abc123xy", + request_uri: "https://example.com/callback" + } as ItwRemoteRequestPayload; + + const { getByTestId } = renderComponent(partialPayload); + + expect(getByTestId("failure")).toBeTruthy(); + }); + + it("should render failure screen if required fields are empty", () => { + const partialPayload = { + client_id: "", + request_uri: "https://example.com/callback", + state: "hyqizm592" + } as ItwRemoteRequestPayload; + + const { getByTestId } = renderComponent(partialPayload); + + expect(getByTestId("failure")).toBeTruthy(); + }); + + const renderComponent = (payload: Partial = {}) => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const mockNavigation = new Proxy( + {}, + { + get: _ => jest.fn() + } + ) as unknown as IOStackNavigationProp< + ItwRemoteParamsList, + "ITW_REMOTE_REQUEST_VALIDATION" + >; + + const route = { + key: "ITW_REMOTE_REQUEST_VALIDATION", + name: ITW_REMOTE_ROUTES.REQUEST_VALIDATION, + params: payload + }; + + const logic = itwRemoteMachine.provide({ + guards: { + isWalletActive: jest.fn().mockReturnValue(true) + }, + actions: { + navigateToClaimsDisclosureScreen: jest.fn() + } + }); + + return renderScreenWithNavigationStoreContext( + () => ( + + + + ), + ITW_REMOTE_ROUTES.REQUEST_VALIDATION, + payload, + createStore(appReducer, globalState as any) + ); + }; +}); diff --git a/ts/features/landingScreenMultiBanner/store/__tests__/__snapshots__/reducer.test.ts.snap b/ts/features/landingScreenMultiBanner/store/__tests__/__snapshots__/reducer.test.ts.snap index f1f67f85871..bb9430be280 100644 --- a/ts/features/landingScreenMultiBanner/store/__tests__/__snapshots__/reducer.test.ts.snap +++ b/ts/features/landingScreenMultiBanner/store/__tests__/__snapshots__/reducer.test.ts.snap @@ -3,6 +3,7 @@ exports[`landingScreenBannersReducer should match snapshot: undefined_no_action 1`] = ` { "ITW_DISCOVERY": true, + "LV_EXPIRATION_REMINDER": true, "PUSH_NOTIFICATIONS_REMINDER": true, "SETTINGS_DISCOVERY": true, } diff --git a/ts/features/landingScreenMultiBanner/utils/landingScreenBannerMap.tsx b/ts/features/landingScreenMultiBanner/utils/landingScreenBannerMap.tsx index 2beb03129ec..5708298cb15 100644 --- a/ts/features/landingScreenMultiBanner/utils/landingScreenBannerMap.tsx +++ b/ts/features/landingScreenMultiBanner/utils/landingScreenBannerMap.tsx @@ -6,6 +6,8 @@ import { isItwPersistedDiscoveryBannerRenderableSelector } from "../../itwallet/ import { hasUserAcknowledgedSettingsBannerSelector } from "../../profileSettings/store/selectors"; import { PushNotificationsBanner } from "../../pushNotifications/components/PushNotificationsBanner"; import { isPushNotificationsBannerRenderableSelector } from "../../pushNotifications/store/selectors"; +import { LoginExpirationBanner } from "../../login/preferences/components/LoginExpirationBanner"; +import { isSessionExpirationBannerRenderableSelector } from "../../login/preferences/store/selectors"; type ComponentWithCloseHandler = (closeHandler: () => void) => ReactElement; type ComponentAndLogic = { @@ -22,7 +24,8 @@ export type LandingScreenBannerId = export const LANDING_SCREEN_BANNERS_ENABLED_MAP = { PUSH_NOTIFICATIONS_REMINDER: true, ITW_DISCOVERY: true, - SETTINGS_DISCOVERY: true + SETTINGS_DISCOVERY: true, + LV_EXPIRATION_REMINDER: true } as const; export const landingScreenBannerMap: BannerMapById = { @@ -44,5 +47,11 @@ export const landingScreenBannerMap: BannerMapById = { ), isRenderableSelector: (state: GlobalState) => !hasUserAcknowledgedSettingsBannerSelector(state) + }, + LV_EXPIRATION_REMINDER: { + component: closeHandler => ( + + ), + isRenderableSelector: isSessionExpirationBannerRenderableSelector } } as const; diff --git a/ts/features/login/preferences/components/LoginExpirationBanner.tsx b/ts/features/login/preferences/components/LoginExpirationBanner.tsx new file mode 100644 index 00000000000..ecdaad3c4de --- /dev/null +++ b/ts/features/login/preferences/components/LoginExpirationBanner.tsx @@ -0,0 +1,77 @@ +import { + Banner, + IOVisualCostants, + useIOToast +} from "@pagopa/io-app-design-system"; +import { useCallback } from "react"; +import { StyleSheet, View } from "react-native"; +import { useRoute } from "@react-navigation/native"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { openWebUrl } from "../../../../utils/url"; +import I18n from "../../../../i18n"; +import { closeSessionExpirationBanner } from "../store/actions"; +import { formattedExpirationDateSelector } from "../../../../store/reducers/authentication"; +import { helpCenterHowToDoWhenSessionIsExpiredUrl } from "../../../../config"; +import { trackHelpCenterCtaTapped } from "../../../../utils/analytics"; + +const HC_ID = "SESSION_ABOUT_TO_EXPIRE"; + +type Props = { + handleOnClose: () => void; +}; +/** + * to use in case the banner's visibility has to be handled externally + * (see MultiBanner feature for the landing screen) + */ +export const LoginExpirationBanner = ({ handleOnClose }: Props) => { + const { name: routeName } = useRoute(); + const expirationDate = useIOSelector(formattedExpirationDateSelector); + const { error } = useIOToast(); + const dispatch = useIODispatch(); + + const handleOnPress = useCallback(() => { + trackHelpCenterCtaTapped( + HC_ID, + helpCenterHowToDoWhenSessionIsExpiredUrl, + routeName + ); + openWebUrl(helpCenterHowToDoWhenSessionIsExpiredUrl, () => { + error(I18n.t("global.jserror.title")); + }); + }, [error, routeName]); + + const closeHandler = useCallback(() => { + dispatch(closeSessionExpirationBanner()); + handleOnClose(); + }, [dispatch, handleOnClose]); + + return ( + + + + ); +}; + +const styles = StyleSheet.create({ + margins: { + marginHorizontal: IOVisualCostants.appMarginDefault, + marginVertical: 16 + } +}); diff --git a/ts/features/login/preferences/store/__tests__/selectors.test.ts b/ts/features/login/preferences/store/__tests__/selectors.test.ts new file mode 100644 index 00000000000..686eb571ec5 --- /dev/null +++ b/ts/features/login/preferences/store/__tests__/selectors.test.ts @@ -0,0 +1,159 @@ +import { merge, set } from "lodash"; +import * as O from "fp-ts/lib/Option"; +import { addDays } from "date-fns"; +import MockDate from "mockdate"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { isSessionExpirationBannerRenderableSelector } from "../selectors"; +import { isFastLoginEnabledSelector } from "../../../../fastLogin/store/selectors"; +import { AuthenticationState } from "../../../../../store/reducers/authentication"; +import { BackendStatus } from "../../../../../../definitions/content/BackendStatus"; + +// If you want to know when this selector was implemented, you can refer to the mocked date below! +MockDate.set(new Date(2025, 1, 19)); + +const getFutureDate = (days: number): Date => addDays(new Date(), days); + +jest.mock("../../../../fastLogin/store/selectors", () => ({ + isFastLoginEnabledSelector: jest.fn() +})); + +describe("isSessionExpirationBannerRenderableSelector", () => { + afterEach(jest.clearAllMocks); + + it("Should return false when remoteConfig is none", () => { + (isFastLoginEnabledSelector as unknown as jest.Mock).mockReturnValue(true); + + const globalState = configureGlobalState({ + authentication: generateAuthenticationObj(getFutureDate(15)), + remoteConfig: O.none, + showBanner: true + }); + + const isSessionExpirationBannerRenderable = + isSessionExpirationBannerRenderableSelector(globalState); + + expect(isSessionExpirationBannerRenderable).toBe(false); + }); + + it("Should return false when sessionInfo is missing", () => { + (isFastLoginEnabledSelector as unknown as jest.Mock).mockReturnValue(true); + + const globalState = configureGlobalState({ + authentication: generateAuthenticationObj(), + remoteConfig: O.some({ + loginConfig: { + notifyExpirationThreshold: { + fastLogin: 15 + } + } + }), + showBanner: true + }); + + const isSessionExpirationBannerRenderable = + isSessionExpirationBannerRenderableSelector(globalState); + + expect(isSessionExpirationBannerRenderable).toBe(false); + }); + it("Should return false when sessionInfo is present but it doesn't contain expirationDate", () => { + (isFastLoginEnabledSelector as unknown as jest.Mock).mockReturnValue(true); + + const globalState = configureGlobalState({ + authentication: { + kind: "LoggedInWithSessionInfo", + sessionInfo: {} + }, + remoteConfig: O.some({ + loginConfig: { + notifyExpirationThreshold: { + fastLogin: 15 + } + } + }), + showBanner: true + }); + + const isSessionExpirationBannerRenderable = + isSessionExpirationBannerRenderableSelector(globalState); + + expect(isSessionExpirationBannerRenderable).toBe(false); + }); + + it.each` + isFastLogin | showBanner | days | fastLogin | standardLogin | expected + ${true} | ${true} | ${15} | ${30} | ${undefined} | ${true} + ${false} | ${true} | ${3} | ${undefined} | ${3} | ${true} + ${true} | ${true} | ${15} | ${undefined} | ${3} | ${false} + ${true} | ${true} | ${15} | ${15} | ${undefined} | ${true} + ${false} | ${true} | ${30} | ${15} | ${3} | ${false} + ${false} | ${false} | ${3} | ${15} | ${3} | ${false} + ${true} | ${false} | ${3} | ${15} | ${3} | ${false} + `( + "Should return $expected for fastLogin=$isFastLogin, showBanner=$showBanner and expirations in $days days for values { standardLogin: $standardLogin, fastLogin: $fastLogin }", + ({ days, isFastLogin, expected, fastLogin, standardLogin, showBanner }) => { + (isFastLoginEnabledSelector as unknown as jest.Mock).mockReturnValue( + isFastLogin + ); + + const globalState = configureGlobalState({ + authentication: generateAuthenticationObj(getFutureDate(days)), + remoteConfig: O.some({ + loginConfig: { + notifyExpirationThreshold: { + fastLogin, + standardLogin + } + } + }), + showBanner + }); + + const isSessionExpirationBannerRenderable = + isSessionExpirationBannerRenderableSelector(globalState); + + expect(isSessionExpirationBannerRenderable).toBe(expected); + } + ); +}); + +type Config = { + authentication?: Partial; + showBanner?: boolean; + remoteConfig?: O.Option>; +}; + +function generateAuthenticationObj( + expirationDate?: Date +): Partial { + if (expirationDate instanceof Date) { + return { + kind: "LoggedInWithSessionInfo", + sessionInfo: { + expirationDate + } + }; + } + return { + kind: "LoggedInWithoutSessionInfo" + }; +} + +function configureGlobalState({ + authentication, + remoteConfig, + showBanner = true +}: Config) { + const globalState = appReducer(undefined, applicationChangeState("active")); + + return merge( + globalState, + set( + globalState, + "features.loginFeatures.loginPreferences.showSessionExpirationBanner", + showBanner + ), + set(globalState, "authentication", authentication), + set(globalState, "remoteConfig", remoteConfig) + ); +} diff --git a/ts/features/login/preferences/store/actions/index.ts b/ts/features/login/preferences/store/actions/index.ts new file mode 100644 index 00000000000..9d1c4563a76 --- /dev/null +++ b/ts/features/login/preferences/store/actions/index.ts @@ -0,0 +1,9 @@ +import { ActionType, createStandardAction } from "typesafe-actions"; + +export const closeSessionExpirationBanner = createStandardAction( + "CLOSE_SESSION_EXPIRATION_BANNER" +)(); + +export type LoginPreferencesActions = ActionType< + typeof closeSessionExpirationBanner +>; diff --git a/ts/features/login/preferences/store/reducers/index.ts b/ts/features/login/preferences/store/reducers/index.ts new file mode 100644 index 00000000000..6d0566544ed --- /dev/null +++ b/ts/features/login/preferences/store/reducers/index.ts @@ -0,0 +1,45 @@ +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 { closeSessionExpirationBanner } from "../actions"; +import { loginSuccess } from "../../../../../store/actions/authentication"; + +export type LoginPreferencesState = { + showSessionExpirationBanner: boolean; +}; + +const loginPreferencesInitialState: LoginPreferencesState = { + showSessionExpirationBanner: true +}; + +const loginPreferencesReducer = ( + state: LoginPreferencesState = loginPreferencesInitialState, + action: Action +): LoginPreferencesState => { + switch (action.type) { + case getType(loginSuccess): + return loginPreferencesInitialState; + case getType(closeSessionExpirationBanner): + return { + ...state, + showSessionExpirationBanner: false + }; + default: + return state; + } +}; + +const CURRENT_REDUX_OPT_IN_STORE_VERSION = -1; + +const persistConfig: PersistConfig = { + key: "loginPreferences", + storage: AsyncStorage, + version: CURRENT_REDUX_OPT_IN_STORE_VERSION, + whitelist: ["showSessionExpirationBanner"] +}; + +export const loginPreferencesPersistor = persistReducer< + LoginPreferencesState, + Action +>(persistConfig, loginPreferencesReducer); diff --git a/ts/features/login/preferences/store/selectors/index.ts b/ts/features/login/preferences/store/selectors/index.ts new file mode 100644 index 00000000000..e17e2ca3431 --- /dev/null +++ b/ts/features/login/preferences/store/selectors/index.ts @@ -0,0 +1,55 @@ +import { createSelector } from "reselect"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import { differenceInDays } from "date-fns"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { sessionInfoSelector } from "../../../../../store/reducers/authentication"; +import { remoteConfigSelector } from "../../../../../store/reducers/backendStatus/remoteConfig"; +import { isFastLoginEnabledSelector } from "../../../../fastLogin/store/selectors"; + +/** + * This selector returns a boolean that indicates whether to show + * the sessionExpirationBanner or not + */ +const showSessionExpirationBannerSelector = (state: GlobalState) => + state.features.loginFeatures.loginPreferences.showSessionExpirationBanner; + +/** + * This selector combines control over the value of `expirationDate`, + * ensuring that the difference with the actual date at the time of the check + * is less than a specified value in days (e.g., 30), + * with the value related to the `showSessionExpirationBanner` data. + */ +export const isSessionExpirationBannerRenderableSelector = createSelector( + sessionInfoSelector, + remoteConfigSelector, + showSessionExpirationBannerSelector, + isFastLoginEnabledSelector, + (sessionInfo, config, showSessionExpirationBanner, isFastLogin) => + pipe( + O.Do, + O.bind("expirationDate", () => + pipe( + sessionInfo, + O.chainNullableK(({ expirationDate }) => expirationDate) + ) + ), + O.bind("threshold", () => + pipe( + config, + O.chainNullableK(({ loginConfig }) => + isFastLogin + ? loginConfig?.notifyExpirationThreshold?.fastLogin + : loginConfig?.notifyExpirationThreshold?.standardLogin + ) + ) + ), + O.map( + ({ expirationDate, threshold }) => + threshold >= 0 && + showSessionExpirationBanner && + differenceInDays(expirationDate, new Date()) <= threshold + ), + O.getOrElse(() => false) + ) +); diff --git a/ts/features/zendesk/utils/index.ts b/ts/features/zendesk/utils/index.ts index c4ce2883691..0fbd3314030 100644 --- a/ts/features/zendesk/utils/index.ts +++ b/ts/features/zendesk/utils/index.ts @@ -72,7 +72,10 @@ export const formatRequestedTokenString = ( "walletToken", "bpdToken", "fimsToken", - "lollipopAssertionRef" + "lollipopAssertionRef", + // TODO: Evaluate whether it makes sense to keep this value among the default tokens or not. + // Depends on https://pagopa.atlassian.net/browse/IOPID-2750 + "expirationDate" ]; tokensArray = defaultTokens; } diff --git a/ts/store/actions/types.ts b/ts/store/actions/types.ts index 4d8e232ab31..eba0c72243f 100644 --- a/ts/store/actions/types.ts +++ b/ts/store/actions/types.ts @@ -35,6 +35,7 @@ import { IngressScreenActions } from "../../features/ingress/store/actions"; import { MixpanelFeatureActions } from "../../features/mixpanel/store/actions"; import { LandingScreenBannerActions } from "../../features/landingScreenMultiBanner/store/actions"; import { SpidConfigActions } from "../../features/spidLogin/store/actions"; +import { LoginPreferencesActions } from "../../features/login/preferences/store/actions"; import { AnalyticsActions } from "./analytics"; import { ApplicationActions } from "./application"; import { AuthenticationActions } from "./authentication"; @@ -107,7 +108,8 @@ export type Action = | IngressScreenActions | MixpanelFeatureActions | LandingScreenBannerActions - | SpidConfigActions; + | SpidConfigActions + | LoginPreferencesActions; export type Dispatch = DispatchAPI; diff --git a/ts/store/reducers/authentication.ts b/ts/store/reducers/authentication.ts index 2a1edd5ae56..8f55ac6d7d8 100644 --- a/ts/store/reducers/authentication.ts +++ b/ts/store/reducers/authentication.ts @@ -2,6 +2,7 @@ import * as O from "fp-ts/lib/Option"; import { PersistPartial } from "redux-persist"; import { createSelector } from "reselect"; import { isActionOf } from "typesafe-actions"; +import { pipe } from "fp-ts/lib/function"; import { PublicSession } from "../../../definitions/session_manager/PublicSession"; import { SessionToken } from "../../types/SessionToken"; import { @@ -18,6 +19,7 @@ import { import { Action } from "../actions/types"; import { SpidIdp } from "../../../definitions/content/SpidIdp"; import { refreshSessionToken } from "../../features/fastLogin/store/actions/tokenRefreshActions"; +import { format } from "../../utils/dates"; import { logoutRequest } from "./../actions/authentication"; import { GlobalState } from "./types"; @@ -166,6 +168,17 @@ export const sessionInfoSelector = createSelector( : O.none ); +export const formattedExpirationDateSelector = createSelector( + sessionInfoSelector, + sessionInfo => + pipe( + sessionInfo, + O.chainNullableK(({ expirationDate }) => expirationDate), + O.map(expirationDate => format(expirationDate, "D MMMM")), + O.getOrElse(() => "N/A") + ) +); + export const zendeskTokenSelector = (state: GlobalState): string | undefined => isLoggedInWithSessionInfo(state.authentication) ? state.authentication.sessionInfo.zendeskToken diff --git a/ts/store/reducers/index.ts b/ts/store/reducers/index.ts index 9dcad9d883f..521c66da943 100644 --- a/ts/store/reducers/index.ts +++ b/ts/store/reducers/index.ts @@ -246,6 +246,9 @@ export function createRootReducer( state.features.loginFeatures.cieLogin .isCieIDTourGuideEnabled, _persist: state.features.loginFeatures.cieLogin._persist + }, + loginPreferences: { + ...state.features.loginFeatures.loginPreferences } }, profileSettings: {