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 81dd7b4bcb0..dd89f8fa1e0 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 @@ -171,6 +171,21 @@ exports[`featuresPersistor should match snapshot 1`] = ` "nativeLogin": { "enabled": true, }, + "spidLogin": { + "nativeLogin": { + "requestInfo": { + "nativeAttempts": 0, + "requestState": "LOADING", + }, + }, + "standardLogin": { + "requestInfo": { + "requestState": { + "kind": "PotNoneLoading", + }, + }, + }, + }, "testLogin": { "kind": "idle", }, diff --git a/ts/features/common/store/reducers/index.ts b/ts/features/common/store/reducers/index.ts index a2ea0e60c39..978935f24c1 100644 --- a/ts/features/common/store/reducers/index.ts +++ b/ts/features/common/store/reducers/index.ts @@ -56,6 +56,10 @@ import { landingScreenBannersReducer, LandingScreenBannerState } from "../../../landingScreenMultiBanner/store/reducer"; +import { + spidLoginReducer, + SpidLoginState +} from "../../../spidLogin/store/reducers"; type LoginFeaturesState = { testLogin: TestLoginState; @@ -63,6 +67,7 @@ type LoginFeaturesState = { fastLogin: FastLoginState; cieLogin: CieLoginState & PersistPartial; loginInfo: LoginInfoState; + spidLogin: SpidLoginState; }; export type FeaturesState = { @@ -96,7 +101,8 @@ const rootReducer = combineReducers({ nativeLogin: nativeLoginReducer, fastLogin: fastLoginReducer, cieLogin: cieLoginPersistor, - loginInfo: loginInfoReducer + loginInfo: loginInfoReducer, + spidLogin: spidLoginReducer }), wallet: walletReducer, fims: fimsReducer, diff --git a/ts/features/spidLogin/store/actions/index.ts b/ts/features/spidLogin/store/actions/index.ts new file mode 100644 index 00000000000..0abd58cbcdc --- /dev/null +++ b/ts/features/spidLogin/store/actions/index.ts @@ -0,0 +1,24 @@ +import { ActionType, createStandardAction } from "typesafe-actions"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { ErrorType, NativeLoginRequestInfo } from "../../types"; + +export const setNativeLoginRequestInfo = createStandardAction( + "SET_NATIVE_LOGIN_REQUEST_INFO" +)(); +export const incrementNativeLoginNativeAttempts = createStandardAction( + "INCREMENT_NATIVE_LOGIN_NATIVE_ATTEMPTS" +)(); +export const setStandardLoginRequestState = createStandardAction( + "SET_STNDARD_LOGIN_REQUEST_STATE" +)>(); +export const setStandardLoginInLoadingState = createStandardAction( + "SET_STANDARD_LOGIN_IN_LOADING_STATE" +)(); +export const resetSpidLoginState = createStandardAction("RESET_LOGIN_STATE")(); + +export type SpidConfigActions = + | ActionType + | ActionType + | ActionType + | ActionType + | ActionType; diff --git a/ts/features/spidLogin/store/reducers/index.ts b/ts/features/spidLogin/store/reducers/index.ts new file mode 100644 index 00000000000..bfd8477ff86 --- /dev/null +++ b/ts/features/spidLogin/store/reducers/index.ts @@ -0,0 +1,87 @@ +import { getType } from "typesafe-actions"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { Action } from "../../../../store/actions/types"; +import { StandardLoginRequestInfo, NativeLoginRequestInfo } from "../../types"; +import { + incrementNativeLoginNativeAttempts, + setStandardLoginRequestState, + setNativeLoginRequestInfo, + setStandardLoginInLoadingState, + resetSpidLoginState +} from "../actions"; + +export type SpidLoginState = { + nativeLogin: { + requestInfo: NativeLoginRequestInfo; + }; + standardLogin: { + requestInfo: StandardLoginRequestInfo; + }; +}; + +const spidLoginInitialState: SpidLoginState = { + nativeLogin: { + requestInfo: { + requestState: "LOADING", + nativeAttempts: 0 + } + }, + standardLogin: { + requestInfo: { + requestState: pot.noneLoading + } + } +}; + +export const spidLoginReducer = ( + state: SpidLoginState = spidLoginInitialState, + action: Action +): SpidLoginState => { + switch (action.type) { + case getType(setNativeLoginRequestInfo): + return { + ...state, + nativeLogin: { + ...state.nativeLogin, + requestInfo: action.payload + } + }; + case getType(incrementNativeLoginNativeAttempts): + return { + ...state, + nativeLogin: { + ...state.nativeLogin, + requestInfo: { + requestState: "LOADING", + nativeAttempts: state.nativeLogin.requestInfo.nativeAttempts + 1 + } + } + }; + case getType(setStandardLoginRequestState): + return { + ...state, + standardLogin: { + ...state.standardLogin, + requestInfo: { + ...state.standardLogin.requestInfo, + requestState: action.payload + } + } + }; + case getType(setStandardLoginInLoadingState): + return { + ...state, + standardLogin: { + ...state.standardLogin, + requestInfo: { + ...state.standardLogin.requestInfo, + requestState: pot.noneLoading + } + } + }; + case getType(resetSpidLoginState): + return spidLoginInitialState; + default: + return state; + } +}; diff --git a/ts/features/spidLogin/store/selectors/index.ts b/ts/features/spidLogin/store/selectors/index.ts new file mode 100644 index 00000000000..5a9a9e14d72 --- /dev/null +++ b/ts/features/spidLogin/store/selectors/index.ts @@ -0,0 +1,15 @@ +import { createSelector } from "reselect"; +import { GlobalState } from "../../../../store/reducers/types"; + +export const spidLoginSelector = (state: GlobalState) => + state.features.loginFeatures.spidLogin; + +export const nativeLoginRequestInfoSelector = createSelector( + spidLoginSelector, + ({ nativeLogin }) => nativeLogin.requestInfo +); + +export const standardLoginRequestInfoSelector = createSelector( + spidLoginSelector, + ({ standardLogin }) => standardLogin.requestInfo +); diff --git a/ts/features/spidLogin/types/index.ts b/ts/features/spidLogin/types/index.ts new file mode 100644 index 00000000000..497e41bac48 --- /dev/null +++ b/ts/features/spidLogin/types/index.ts @@ -0,0 +1,26 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; + +export enum ErrorType { + "LOADING_ERROR" = "LOADING_ERROR", + "LOGIN_ERROR" = "LOGIN_ERROR" +} + +export type RequestInfoPositiveStates = { + requestState: "LOADING" | "AUTHORIZED" | "AUTHORIZING"; + nativeAttempts: number; +}; + +export type RequestInfoError = { + requestState: "ERROR"; + errorType: ErrorType; + errorCodeOrMessage?: string; + nativeAttempts: number; +}; + +export type NativeLoginRequestInfo = + | RequestInfoPositiveStates + | RequestInfoError; + +export type StandardLoginRequestInfo = { + requestState: pot.Pot; +}; diff --git a/ts/screens/authentication/AuthErrorScreen.tsx b/ts/screens/authentication/AuthErrorScreen.tsx index a8cca27fde7..cf101bed103 100644 --- a/ts/screens/authentication/AuthErrorScreen.tsx +++ b/ts/screens/authentication/AuthErrorScreen.tsx @@ -8,6 +8,12 @@ import { useIONavigation } from "../../navigation/params/AppParamsList"; import ROUTES from "../../navigation/routes"; import { CieIdLoginProps } from "../../features/cieLogin/components/CieIdLoginWebView"; import { AuthenticationParamsList } from "../../navigation/params/AuthenticationParamsList"; +import { useIODispatch } from "../../store/hooks"; +import { + incrementNativeLoginNativeAttempts, + resetSpidLoginState, + setStandardLoginInLoadingState +} from "../../features/spidLogin/store/actions"; import { UnlockAccessProps } from "./UnlockAccessComponent"; import AuthErrorComponent from "./components/AuthErrorComponent"; @@ -17,12 +23,11 @@ type CommonAuthErrorScreenProps = { type SpidProps = { authMethod: "SPID"; - onRetry: () => void; + isNativeLogin?: boolean; }; type CieIdProps = { authMethod: "CIE_ID"; - onRetry?: () => void; params: CieIdLoginProps; }; @@ -40,6 +45,7 @@ const authScreenByAuthMethod = { }; const AuthErrorScreen = () => { + const dispatch = useIODispatch(); const route = useRoute>(); const { errorCodeOrMessage, authMethod, authLevel } = route.params; @@ -61,17 +67,22 @@ const AuthErrorScreen = () => { }, [authMethod, route.params]); const onRetry = useCallback(() => { - if (authMethod === "SPID" || authMethod === "CIE_ID") { - route.params.onRetry?.(); + if (authMethod === "SPID") { + dispatch( + route.params.isNativeLogin + ? incrementNativeLoginNativeAttempts() + : setStandardLoginInLoadingState() + ); } navigation.navigate(ROUTES.AUTHENTICATION, getNavigationParams()); - }, [authMethod, navigation, route.params, getNavigationParams]); + }, [authMethod, navigation, route.params, getNavigationParams, dispatch]); const onCancel = useCallback(() => { + dispatch(resetSpidLoginState()); navigation.navigate(ROUTES.AUTHENTICATION, { screen: ROUTES.AUTHENTICATION_LANDING }); - }, [navigation]); + }, [navigation, dispatch]); return ( { const dispatch = useIODispatch(); - const { navigate } = useIONavigation(); + // The choice was made to use `replace` instead of `navigate` because the former unmounts the current screen, + // ensuring the re-execution of the `useLollipopLoginSource` hook. + const { replace } = useIONavigation(); const selectedIdp = useIOSelector(selectedIdentityProviderSelector, _isEqual); const selectedIdpTextData = useIOSelector( idpContextualHelpDataFromIdSelector(selectedIdp?.id), @@ -95,25 +99,27 @@ const IdpLoginScreen = () => { _isEqual ); - const [requestState, setRequestState] = useState>( - pot.noneLoading - ); + const { requestState } = useIOSelector(standardLoginRequestInfoSelector); const [errorCodeOrMessage, setErrorCodeOrMessage] = useState< string | undefined >(undefined); const [loginTrace, setLoginTrace] = useState(undefined); + const setRequestState = useCallback( + (req: pot.Pot) => { + dispatch(setStandardLoginRequestState(req)); + }, + [dispatch] + ); + const handleOnLollipopCheckFailure = useCallback(() => { setRequestState(pot.noneError(ErrorType.LOGIN_ERROR)); - }, []); + }, [setRequestState]); const idpId = loggedOutWithIdpAuth?.idp.id; const loginUri = idpId ? getIdpLoginUri(idpId, 2) : undefined; - const { - retryLollipopLogin, - shouldBlockUrlNavigationWhileCheckingLollipop, - webviewSource - } = useLollipopLoginSource(handleOnLollipopCheckFailure, loginUri); + const { shouldBlockUrlNavigationWhileCheckingLollipop, webviewSource } = + useLollipopLoginSource(handleOnLollipopCheckFailure, loginUri); const choosenTool = useMemo( () => assistanceToolRemoteConfig(assistanceToolConfig), @@ -138,7 +144,7 @@ const IdpLoginScreen = () => { setRequestState(pot.noneError(ErrorType.LOADING_ERROR)); } }, - [loggedOutWithIdpAuth?.idp.id] + [loggedOutWithIdpAuth?.idp.id, setRequestState] ); const handleLoginFailure = useCallback( @@ -170,7 +176,7 @@ const IdpLoginScreen = () => { setRequestState(pot.noneError(ErrorType.LOGIN_ERROR)); setErrorCodeOrMessage(code || message); }, - [dispatch, choosenTool, idp] + [dispatch, choosenTool, idp, setRequestState] ); const handleLoginSuccess = useCallback( @@ -183,11 +189,6 @@ const IdpLoginScreen = () => { [choosenTool, dispatch, idp] ); - const onRetryButtonPressed = useCallback((): void => { - setRequestState(pot.noneLoading); - retryLollipopLogin(); - }, [retryLollipopLogin]); - const handleNavigationStateChange = useCallback( (event: WebViewNavigation) => { const url = event.url; @@ -212,7 +213,7 @@ const IdpLoginScreen = () => { event.loading || isAssertion ? pot.noneLoading : pot.some(true) ); }, - [dispatch, loginTrace] + [dispatch, loginTrace, setRequestState] ); const handleShouldStartLoading = useCallback( @@ -265,16 +266,17 @@ const IdpLoginScreen = () => { }; const navigateToAuthErrorScreen = useCallback(() => { - navigate(ROUTES.AUTHENTICATION, { + // The choice was made to use `replace` instead of `navigate` because the former unmounts the current screen, + // ensuring the re-execution of the `useLollipopLoginSource` hook. + replace(ROUTES.AUTHENTICATION, { screen: ROUTES.AUTH_ERROR_SCREEN, params: { errorCodeOrMessage, authMethod: "SPID", - authLevel: "L2", - onRetry: onRetryButtonPressed + authLevel: "L2" } }); - }, [errorCodeOrMessage, onRetryButtonPressed, navigate]); + }, [errorCodeOrMessage, replace]); useEffect(() => { if (pot.isError(requestState)) { diff --git a/ts/screens/authentication/idpAuthSessionHandler.tsx b/ts/screens/authentication/idpAuthSessionHandler.tsx index 62902134f9d..c2444f98ddd 100644 --- a/ts/screens/authentication/idpAuthSessionHandler.tsx +++ b/ts/screens/authentication/idpAuthSessionHandler.tsx @@ -11,7 +11,7 @@ import * as O from "fp-ts/lib/Option"; import * as T from "fp-ts/lib/Task"; import * as TE from "fp-ts/lib/TaskEither"; import * as React from "react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { AppState, SafeAreaView, StyleSheet, View } from "react-native"; import I18n from "../../i18n"; import { mixpanelTrack } from "../../mixpanel"; @@ -59,6 +59,8 @@ import { handleSendAssistanceLog } from "../../utils/supportAssistance"; import { emptyContextualHelp } from "../../utils/emptyContextualHelp"; +import { nativeLoginRequestInfoSelector } from "../../features/spidLogin/store/selectors"; +import { setNativeLoginRequestInfo } from "../../features/spidLogin/store/actions"; const styles = StyleSheet.create({ errorContainer: { @@ -115,13 +117,17 @@ const idpAuthSession = ( // This page is used in the native login process. export const AuthSessionPage = () => { - const [requestInfo, setRequestInfo] = useState({ - requestState: "LOADING", - nativeAttempts: 0 - }); - + const dispatch = useIODispatch(); + const requestInfo = useIOSelector(nativeLoginRequestInfoSelector); const mixpanelEnabled = useIOSelector(isMixpanelEnabled); + const setRequestInfo = useCallback( + (reqInfo: RequestInfo) => { + dispatch(setNativeLoginRequestInfo(reqInfo)); + }, + [dispatch] + ); + // This is a handler for the browser login. It applies to android only. useEffect(() => { const subscription = AppState.addEventListener("change", nextAppState => { @@ -136,9 +142,7 @@ export const AuthSessionPage = () => { return () => { subscription.remove(); }; - }, [requestInfo.nativeAttempts]); - - const dispatch = useIODispatch(); + }, [requestInfo.nativeAttempts, setRequestInfo]); // We call useIOStore beacause we only need some values from store, we don't need any re-render logic const store = useIOStore(); @@ -223,7 +227,7 @@ export const AuthSessionPage = () => { nativeAttempts: requestInfo.nativeAttempts }); }, - [choosenTool, dispatch, idp, requestInfo.nativeAttempts] + [choosenTool, dispatch, idp, requestInfo.nativeAttempts, setRequestInfo] ); const handleLoginSuccess = useCallback( @@ -237,7 +241,14 @@ export const AuthSessionPage = () => { ? dispatch(loginSuccess({ token, idp })) : handleLoginFailure("n/a"); }, - [choosenTool, dispatch, handleLoginFailure, idp, requestInfo.nativeAttempts] + [ + choosenTool, + dispatch, + handleLoginFailure, + idp, + requestInfo.nativeAttempts, + setRequestInfo + ] ); // This function is executed when the native component resolve with an error or when loginUri is undefined. // About the first case, unless there is a problem with the phone crashing for other reasons, this is very unlikely to happen. @@ -268,7 +279,7 @@ export const AuthSessionPage = () => { nativeAttempts: requestInfo.nativeAttempts }); }, - [dispatch, idp, requestInfo.nativeAttempts] + [dispatch, idp, requestInfo.nativeAttempts, setRequestInfo] ); // Memoized values/func --end-- @@ -364,13 +375,6 @@ export const AuthSessionPage = () => { faqCategories: ["authentication_SPID"], canGoBack: isBackButtonEnabled(requestInfo) }); - // It is enough to set the status to loading, - // the reload will ensure that the functions necessary for correct functioning are performed. - const onRetry = () => - setRequestInfo({ - requestState: "LOADING", - nativeAttempts: requestInfo.nativeAttempts + 1 - }); if (requestInfo.requestState === "ERROR") { navigation.navigate(ROUTES.AUTHENTICATION, { @@ -379,7 +383,7 @@ export const AuthSessionPage = () => { errorCodeOrMessage: requestInfo.errorCodeOrMessage, authMethod: "SPID", authLevel: "L2", - onRetry + isNativeLogin: true } }); } diff --git a/ts/store/actions/types.ts b/ts/store/actions/types.ts index 83ff7012be7..4d8e232ab31 100644 --- a/ts/store/actions/types.ts +++ b/ts/store/actions/types.ts @@ -34,6 +34,7 @@ import { ProfileSettingsActions } from "../../features/profileSettings/store/act 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 { AnalyticsActions } from "./analytics"; import { ApplicationActions } from "./application"; import { AuthenticationActions } from "./authentication"; @@ -105,7 +106,8 @@ export type Action = | ProfileSettingsActions | IngressScreenActions | MixpanelFeatureActions - | LandingScreenBannerActions; + | LandingScreenBannerActions + | SpidConfigActions; export type Dispatch = DispatchAPI;