Skip to content

Commit

Permalink
chore: [IOPID-966] L2 Locked Access (#5148)
Browse files Browse the repository at this point in the history
## Short description
In this PR, the screen that is shown to a user who tries to log into IO
but has blocked access has been realised. (access is blocked via the web
portal).
The change includes adding within the dev server (and BE) the error 1002
received after logging in with the IDP (spid or cie)
The change within the dev-server is in [this
PR](pagopa/io-dev-api-server#311)


https://github.com/pagopa/io-app/assets/83651704/583bbacc-bca0-436e-a51a-1d4f6168a618


## List of changes proposed in this pull request
- Realization of the UnlockAccessScreen.tsx screen
([Figma](https://www.figma.com/file/2BZhyc0qxN31sF6npvbeTh/IO-Esco?type=design&node-id=21-12453&mode=design&t=mZBnBsYJw7xmO7WA-0))
- Realization of the UnlockAccessScreen.test.tsx
- Business logic
- Integration of texts and translations


## How to test
In the .env file, set CIE_LOGIN_WITH_DEV_SERVER_ENABLED as YES and run
the project. Log in with CIE or SPID and select the error "Utente con
identità bloccata da ioapp.it"

---------

Co-authored-by: Fabio Bombardi <[email protected]>
  • Loading branch information
Ladirico and shadowsheep1 authored Oct 30, 2023
1 parent c5a8e47 commit 6142a46
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 25 deletions.
22 changes: 22 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,28 @@ authentication:
error_1001: "Access Denied: You are not of the minimum age required to use IO"
expiredSessionBanner:
title: Security notice
message: To keep your private information safe, please login again with your SPID credentials, thanks!
unlock:
title: Unlock access
subtitlel2: In order to use the app, you must first unlock access to IO.
subtitlel3: To be able to access the app with all your SPID or CIE identities, unlock access.
learnmore: Learn more
loginIO: Close
unlockmodal:
title: What does it mean?
description1_1: If you have blocked access to IO for security reasons, you must first unlock it in order to re-enter the app.
description1_2: Otherwise, you can access it in the app by logging in with the
description1_3: maximum level of safety.
description1_4: This type of authentication requires a username, password, and physical device, such as CIE.
title2: How to unlock IO access
listitem1: Make sure you have secured your digital identity by changing your login credentials with your Identity Provider.
listitem2_1: Access IO from the web, using your SPID or CIE credentials.
listitem2_2: Go to the site
listitem3_1: Go to the section
listitem3_2: Unlock access to IO
listitem3_3: and follow the given steps. When prompted enter the
listitem3_4: recovery code
listitem3_5: to confirm the operation.
email:
cduModal:
validateMail:
Expand Down
22 changes: 22 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,28 @@ authentication:
error_1001: "Accesso negato: non hai l'età minima richiesta per usare IO"
expiredSessionBanner:
title: Avviso di sicurezza
message: Per mantenere al sicuro i tuoi dati privati, accedi nuovamente con le tue credenziali SPID, grazie!
unlock:
title: Sblocca l’accesso
subtitlel2: Per poter usare l'app, devi prima sbloccare l'accesso a IO.
subtitlel3: Per poter accedere all’app con tutte le tue identità SPID o CIE, sblocca l’accesso.
learnmore: Scopri di più
loginIO: Chiudi
unlockmodal:
title: Cosa significa?
description1_1: Se hai bloccato l’accesso a IO per motivi di sicurezza, per poter rientrare in app devi prima sbloccarlo.
description1_2: In alternativa, è possibile entrare in app utilizzando un accesso con il
description1_3: livello massimo di sicurezza.
description1_4: Questo tipo di autenticazione richiede un nome utente, una password e un supporto fisico, come ad esempio la CIE.
title2: Come sbloccare l’accesso a IO
listitem1: Assicurati di aver messo in sicurezza la tua identità digitale, modificando le credenziali di accesso con il tuo Identity Provider.
listitem2_1: Accedi a IO dal web, utilizzando le tue credenziali SPID o CIE.
listitem2_2: Vai al sito
listitem3_1: Vai alla sezione
listitem3_2: Sblocca accesso a IO
listitem3_3: e segui i passaggi indicati. Quando richiesto inserisci il
listitem3_4: codice di ripristino
listitem3_5: per confermare l’operazione.
email:
cduModal:
validateMail:
Expand Down
21 changes: 13 additions & 8 deletions ts/screens/authentication/IdpLoginScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
import { getUrlBasepath } from "../../utils/url";
import { IdpData } from "../../../definitions/content/IdpData";
import { trackSpidLoginError } from "../../utils/analytics";
import UnlockAccessScreen from "../onboarding/UnlockAccessScreen";
import { originSchemasWhiteList } from "./originSchemasWhiteList";
import { IdpAuthErrorScreen } from "./idpAuthErrorScreen";

Expand Down Expand Up @@ -217,14 +218,18 @@ const IdpLoginScreen = (props: Props) => {
</View>
);
} else if (pot.isError(requestState)) {
return (
<IdpAuthErrorScreen
requestStateError={requestState.error}
errorCode={errorCode}
onCancel={() => props.navigation.goBack()}
onRetry={onRetryButtonPressed}
/>
);
if (errorCode === "1002") {
return <UnlockAccessScreen identifier="SPID" />;
} else {
return (
<IdpAuthErrorScreen
requestStateError={requestState.error}
errorCode={errorCode}
onCancel={() => props.navigation.goBack()}
onRetry={onRetryButtonPressed}
/>
);
}
}
// loading complete, no mask needed
return null;
Expand Down
37 changes: 21 additions & 16 deletions ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { originSchemasWhiteList } from "../originSchemasWhiteList";
import { GlobalState } from "../../../store/reducers/types";
import { isCieLoginUatEnabledSelector } from "../../../features/cieLogin/store/selectors";
import { withTrailingPoliceCarLightEmojii } from "../../../utils/strings";
import UnlockAccessScreen from "../../onboarding/UnlockAccessScreen";

export type CieConsentDataUsageScreenNavigationParams = {
cieConsentUri: string;
Expand Down Expand Up @@ -150,22 +151,26 @@ class CieConsentDataUsageScreen extends React.Component<Props, State> {
return loaderComponent;
}
if (this.state.hasError) {
const errorTranslationKey = this.state.errorCode
? `authentication.errors.spid.error_${this.state.errorCode}`
: "authentication.errors.network.title";
return (
<GenericErrorComponent
retryButtonTitle={I18n.t(
"authentication.cie.dataUsageConsent.retryCTA"
)}
onRetry={this.props.resetNavigation}
onCancel={undefined}
image={require("../../../../img/broken-link.png")} // TODO: use custom or generic image?
text={I18n.t(errorTranslationKey, {
defaultValue: I18n.t("authentication.errors.spid.unknown")
})}
/>
);
if (this.state.errorCode === "1002") {
return <UnlockAccessScreen identifier="CIE" />;
} else {
const errorTranslationKey = this.state.errorCode
? `authentication.errors.spid.error_${this.state.errorCode}`
: "authentication.errors.network.title";
return (
<GenericErrorComponent
retryButtonTitle={I18n.t(
"authentication.cie.dataUsageConsent.retryCTA"
)}
onRetry={this.props.resetNavigation}
onCancel={undefined}
image={require("../../../../img/broken-link.png")} // TODO: use custom or generic image?
text={I18n.t(errorTranslationKey, {
defaultValue: I18n.t("authentication.errors.spid.unknown")
})}
/>
);
}
} else {
return (
<WebView
Expand Down
152 changes: 152 additions & 0 deletions ts/screens/onboarding/UnlockAccessScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React from "react";
import {
Body,
ButtonLink,
ButtonSolid,
ContentWrapper,
FeatureInfo,
GradientScrollView,
H3,
IOColors,
IOStyles,
LabelSmall,
Pictogram,
VSpacer
} from "@pagopa/io-app-design-system";
import { SafeAreaView } from "react-native-safe-area-context";
import { Text, View } from "react-native";
import { useNavigation } from "@react-navigation/native";
import BaseScreenComponent from "../../components/screens/BaseScreenComponent";
import I18n from "../../i18n";
import { useIOBottomSheetAutoresizableModal } from "../../utils/hooks/bottomSheet";
import { openWebUrl } from "../../utils/url";
import ROUTES from "../../navigation/routes";
type Props = {
identifier: "SPID" | "CIE";
};
const UnlockAccessScreen = (props: Props) => {
const { identifier } = props;
const navigation = useNavigation();
const ModalContent = () => (
<View testID="modal-view-test">
<Body weight="Regular" color="grey-700">
{I18n.t("authentication.unlockmodal.description1_1")}
{"\n"}
{I18n.t("authentication.unlockmodal.description1_2")}{" "}
<Text style={{ color: IOColors["grey-700"], fontWeight: "bold" }}>
{I18n.t("authentication.unlockmodal.description1_3")}{" "}
</Text>
{I18n.t("authentication.unlockmodal.description1_4")}{" "}
</Body>
<VSpacer size={24} />
<H3 weight="SemiBold">{I18n.t("authentication.unlockmodal.title2")}</H3>
<VSpacer size={24} />
<FeatureInfo
iconName="security"
body={I18n.t("authentication.unlockmodal.listitem1")}
/>
<VSpacer size={16} />
<FeatureInfo
iconName="security"
body={I18n.t("authentication.unlockmodal.listitem2_1")}
actionLabel={I18n.t("authentication.unlockmodal.listitem2_2")}
actionOnPress={() => openWebUrl("https://ioapp.it/")}
/>
<VSpacer size={16} />
<FeatureInfo
iconName="security"
body={
<LabelSmall weight="Regular" color="grey-700">
{I18n.t("authentication.unlockmodal.listitem3_1")}{" "}
<Text style={{ fontStyle: "italic" }}>
{I18n.t("authentication.unlockmodal.listitem3_2")}{" "}
</Text>
{I18n.t("authentication.unlockmodal.listitem3_3")}{" "}
<Text style={{ fontWeight: "bold" }}>
{I18n.t("authentication.unlockmodal.listitem3_4")}{" "}
</Text>
{I18n.t("authentication.unlockmodal.listitem3_5")}
</LabelSmall>
}
/>
</View>
);

const {
present: presentVeryLongAutoresizableBottomSheetWithFooter,
bottomSheet: veryLongAutoResizableBottomSheetWithFooter
} = useIOBottomSheetAutoresizableModal(
{
title: I18n.t("authentication.unlockmodal.title"),
component: <ModalContent />,
fullScreen: true
},
100
);

return (
<BaseScreenComponent
goBack={false}
accessibilityEvents={{ avoidNavigationEventsUsage: true }}
>
<GradientScrollView
testID="container-test"
primaryAction={
<ButtonSolid
fullWidth
testID="button-solid-test"
label={I18n.t("authentication.unlock.title")}
accessibilityLabel="Click here to unlock your profile"
onPress={() => openWebUrl("https://ioapp.it/")}
/>
}
secondaryAction={
<ButtonLink
testID="button-link-test"
label={I18n.t("authentication.unlock.loginIO")}
accessibilityLabel="Click here to redirect to the landing screen"
onPress={() => navigation.navigate(ROUTES.AUTHENTICATION_LANDING)}
/>
}
>
<SafeAreaView>
<ContentWrapper>
<View style={IOStyles.selfCenter}>
<Pictogram name="accessDenied" size={120} color="aqua" />
</View>
<VSpacer size={16} />
<View style={IOStyles.alignCenter}>
<H3 testID="title-test" weight="SemiBold">
{I18n.t("authentication.unlock.title")}
</H3>
</View>
<VSpacer size={16} />
<View>
<Body
weight="Regular"
style={{ textAlign: "center" }}
testID="subtitle-test"
color="grey-650"
>
{identifier === "SPID"
? I18n.t("authentication.unlock.subtitlel2")
: I18n.t("authentication.unlock.subtitlel3")}
</Body>
</View>
<VSpacer size={16} />
<View style={IOStyles.selfCenter}>
<ButtonLink
label={I18n.t("authentication.unlock.learnmore")}
onPress={presentVeryLongAutoresizableBottomSheetWithFooter}
testID="learn-more-link-test"
/>
</View>
</ContentWrapper>
{veryLongAutoResizableBottomSheetWithFooter}
</SafeAreaView>
</GradientScrollView>
</BaseScreenComponent>
);
};

export default UnlockAccessScreen;
63 changes: 63 additions & 0 deletions ts/screens/onboarding/__tests__/UnlockAccessScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { fireEvent } from "@testing-library/react-native";
import { createStore } from "redux";
import { applicationChangeState } from "../../../store/actions/application";
import { appReducer } from "../../../store/reducers";
import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapper";
import UnlockAccessScreen from "../UnlockAccessScreen";

const mockOpenWebUrl = jest.fn();

jest.mock("../../../utils/url", () => ({
openWebUrl: (_: string) => {
mockOpenWebUrl();
}
}));

describe("UnlockAccessScreen", async () => {
it("the components into the page should be render correctly", () => {
const component = renderComponent();
expect(component).toBeDefined();
expect(component.getByTestId("container-test")).not.toBeNull();
expect(component.getByTestId("title-test")).toBeDefined();
expect(component.getByTestId("subtitle-test")).toBeDefined();
const learnMoreButton = component.getByTestId("learn-more-link-test");
expect(learnMoreButton).toBeDefined();

const unlockProfileButton = component.getByTestId("button-solid-test");
expect(unlockProfileButton).toBeDefined();
const closeButton = component.getByTestId("button-link-test");
expect(closeButton).toBeDefined();
});
it("click on button to unlock profile", () => {
const component = renderComponent();
expect(component).toBeDefined();
const unlockProfileButton = component.getByTestId("button-solid-test");

if (unlockProfileButton) {
fireEvent.press(unlockProfileButton);
expect(mockOpenWebUrl).toHaveBeenCalled();
}
});
it("click on button to go back to landing page", () => {
const component = renderComponent();
expect(component).toBeDefined();
const closeButton = component.getByTestId("button-link-test");

if (closeButton) {
fireEvent.press(closeButton);
expect(mockOpenWebUrl).toHaveBeenCalled();
}
});
});

const renderComponent = () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
const store = createStore(appReducer, globalState as any);

return renderScreenWithNavigationStoreContext(
UnlockAccessScreen,
"DUMMY",
{},
store
);
};
4 changes: 3 additions & 1 deletion ts/utils/spidErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ const spidErrorCodeTable = new Map<string, string>([
["26", "Processo di erogazione dell’identità digitale andata a buon fine"],
["27", "Utente già presente"],
["28", "Operazione annullata"],
["29", "Identità non erogata"]
["29", "Identità non erogata"],
["1001", "Cittadino minore di 14 anni"],
["1002", "Utente con identità bloccata da ioapp.it"]
]);

export const getSpidErrorCodeDescription = (errorCode: string) =>
Expand Down

0 comments on commit 6142a46

Please sign in to comment.