From 26916b76f9c867a354613d4d6b48543ace4bcb8f Mon Sep 17 00:00:00 2001
From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com>
Date: Wed, 22 May 2024 15:41:25 +0200
Subject: [PATCH 1/5] feat: [IOPID-1548] DS add new cie errors (#5750)
## Short description
Add CIE errors screens. Change generic error design and add errors 22
and 1001.
## List of changes proposed in this pull request
- Fix the incorrect visualization of error 1002 (using a custom
WizardScreen -> create component CustomWizardScreen)
- Add tests
- Add screens for error 1001 and 22 and generic
- Add new route to visualize CIE error
- Change `CieConsentDataUsageScreen` from class component to function
component
- Change folder of UnlockAccessScreen from onboarding to authentication
> [!Tip]
> [![Run e2e
tests](https://github.com/pagopa/io-app/actions/workflows/test-e2e.yml/badge.svg?branch=IOPID-1548-ds-cie-errors)](https://github.com/pagopa/io-app/actions/workflows/test-e2e.yml)
> [!Note]
> 1. To test all error cases, I used Proxyman by forcing the response
that should come from the BE. So the flow is not correct, in fact error
1002 is not displayed with the L3 access type
> 2. The A11Y test were not realised because the same components were
tested in many other PR
> 3. I modified the component for error 1002, which is in common with
SPID, so I additionally tested the spid flow for error 1002.
## iOS screens
| Error 1001 | Error 1002 | Error 22 | Error Generic | SPID Error 1002 |
| - | - | - | - | - |
| | | | | |
## Android screens
| Error 1001 | Error 1002 | Error 22 | Error Generic | SPID Error 1002 |
| - | - | - | - | - |
| | | | | |
## How to test
IOS: before building the app run `yarn cie-ios:prod` and then perform
authentication flow with CIE
Android: run the application using .env.production
---------
Co-authored-by: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com>
Co-authored-by: mariateresaventura <90319761+mariateresaventura@users.noreply.github.com>
---
locales/de/index.yml | 1 -
locales/en/index.yml | 12 +-
locales/it/index.yml | 12 +-
ts/components/screens/CustomWizardScreen.tsx | 126 +++++++
ts/navigation/AuthenticationNavigator.tsx | 12 +
.../params/AuthenticationParamsList.ts | 4 +
ts/navigation/routes.ts | 2 +
ts/screens/authentication/IdpLoginScreen.tsx | 11 +-
.../UnlockAccessComponent.tsx} | 112 +++---
.../authentication/UnlockAccessScreen.tsx | 16 +
.../__tests__/UnlockAccessComponent.test.tsx | 109 ++++++
.../__tests__/UnlockAccessScreen.test.tsx | 54 +++
.../authentication/cie/CieAuthErrorScreen.tsx | 39 +++
.../cie/CieConsentDataUsageScreen.tsx | 319 ++++++++----------
.../cie/__test__/AuthErrorScreen.test.tsx | 70 ++++
.../cie/components/AuthErrorScreen.tsx | 65 ++++
.../__tests__/UnlockAccessScreen.test.tsx | 63 ----
17 files changed, 706 insertions(+), 321 deletions(-)
create mode 100644 ts/components/screens/CustomWizardScreen.tsx
rename ts/screens/{onboarding/UnlockAccessScreen.tsx => authentication/UnlockAccessComponent.tsx} (53%)
create mode 100644 ts/screens/authentication/UnlockAccessScreen.tsx
create mode 100644 ts/screens/authentication/__tests__/UnlockAccessComponent.test.tsx
create mode 100644 ts/screens/authentication/__tests__/UnlockAccessScreen.test.tsx
create mode 100644 ts/screens/authentication/cie/CieAuthErrorScreen.tsx
create mode 100644 ts/screens/authentication/cie/__test__/AuthErrorScreen.test.tsx
create mode 100644 ts/screens/authentication/cie/components/AuthErrorScreen.tsx
delete mode 100644 ts/screens/onboarding/__tests__/UnlockAccessScreen.test.tsx
diff --git a/locales/de/index.yml b/locales/de/index.yml
index a4f53abb282..ebe57cfb63a 100644
--- a/locales/de/index.yml
+++ b/locales/de/index.yml
@@ -700,7 +700,6 @@ authentication:
subtitlel2: "Um die App nutzen zu können, müsst du zunächst den Zugang zu IO entsperren."
subtitlel3: "Um mit all deinen SPID-Anmeldedaten oder deiner CIE auf die App zugreifen zu können, musst du den Zugang entsperren."
learnmore: "Mehr erfahren"
- loginIO: "Schließen"
unlockmodal:
title: "Was bedeutet das?"
description1_1: "Wenn du den Zugang zu IO aus Sicherheitsgründen gesperrt hast, musst du ihn zunächst entsperren, um die App wieder öffnen zu können."
diff --git a/locales/en/index.yml b/locales/en/index.yml
index b5b80c99fc3..18c70b86d43 100644
--- a/locales/en/index.yml
+++ b/locales/en/index.yml
@@ -607,6 +607,16 @@ authentication:
expiredCardHeaderTitle: Login with CIE
expiredCardContent: The card used is no longer valid or has expired. Use SPID to enter the app.
expiredCardHelp: How to renew your Electronic Identity Card?
+ cie_errors:
+ error_22:
+ title: You haven't agreed to share your data
+ subtitle: To enter, you must agree to share some data
+ error_1001:
+ title: Sorry, you don't meet the minimum age requirement
+ subtitle: You must be at least 18 years old
+ generic:
+ title: Sorry, access was not available.
+ subtitle: A problem occurred while logging in. Please try again in a few minutes.
cie:
genericTitle: Login with CIE
cie: CIE
@@ -751,7 +761,7 @@ authentication:
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
+ loginIO: Not now
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.
diff --git a/locales/it/index.yml b/locales/it/index.yml
index 5d4aa87c9d4..af7f473f317 100644
--- a/locales/it/index.yml
+++ b/locales/it/index.yml
@@ -607,6 +607,16 @@ authentication:
expiredCardHeaderTitle: Entra con CIE
expiredCardContent: La carta utilizzata potrebbe essere scaduta o non più valida. Prova ad usare SPID per entrare in app.
expiredCardHelp: Come rinnovare la propria Carta di Identità Elettronica?
+ cie_errors:
+ error_22:
+ title: Non hai dato il consenso all’invio dei dati
+ subtitle: Per accedere, è necessario acconsentire all’invio di alcuni dati.
+ error_1001:
+ title: Non hai l'età minima richiesta
+ subtitle: Per accedere, devi avere almeno 18 anni
+ generic:
+ title: Non è stato possibile accedere
+ subtitle: Si è verificato un problema durante l’accesso. Riprova tra qualche minuto.
cie:
genericTitle: Entra con CIE
cie: CIE
@@ -751,7 +761,7 @@ authentication:
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
+ loginIO: Non ora, entra su IO
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.
diff --git a/ts/components/screens/CustomWizardScreen.tsx b/ts/components/screens/CustomWizardScreen.tsx
new file mode 100644
index 00000000000..95cce99b303
--- /dev/null
+++ b/ts/components/screens/CustomWizardScreen.tsx
@@ -0,0 +1,126 @@
+import {
+ Body,
+ ButtonLink,
+ ButtonLinkProps,
+ ButtonSolid,
+ ButtonSolidProps,
+ ContentWrapper,
+ H3,
+ IOPictograms,
+ IOStyles,
+ Pictogram,
+ VSpacer
+} from "@pagopa/io-app-design-system";
+import { SafeAreaView } from "react-native-safe-area-context";
+import * as React from "react";
+import { StyleSheet, View } from "react-native";
+import {
+ BodyProps,
+ ComposedBodyFromArray
+} from "../core/typography/ComposedBodyFromArray";
+export type CustomWizardScreenProps = {
+ title: string;
+ description?: string | Array;
+ pictogram: IOPictograms;
+ primaryButton: Pick<
+ ButtonSolidProps,
+ "label" | "accessibilityLabel" | "onPress" | "testID"
+ >;
+ actionButton?: Pick<
+ ButtonLinkProps,
+ "label" | "accessibilityLabel" | "onPress" | "testID"
+ >;
+ buttonLink?: Pick<
+ ButtonLinkProps,
+ "label" | "accessibilityLabel" | "onPress" | "testID"
+ >;
+};
+
+/**
+ * A common screen used in a wizard flow to show a pictogram, a title, a description and one or two buttons.
+ */
+const CustomWizardScreen = ({
+ title,
+ description,
+ pictogram,
+ primaryButton,
+ actionButton,
+ buttonLink
+}: CustomWizardScreenProps) => (
+
+
+
+
+ {actionButton && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+
+);
+
+type CustomWizardBodyProps = {
+ title: string;
+ description?: string | Array;
+ pictogram: IOPictograms;
+ buttonLink?: Pick<
+ ButtonLinkProps,
+ "label" | "accessibilityLabel" | "onPress" | "testID"
+ >;
+};
+
+const WizardBody = ({
+ title,
+ description,
+ pictogram,
+ buttonLink
+}: CustomWizardBodyProps) => (
+
+
+
+
+
+
+ {title}
+ {description && (
+ <>
+
+ {typeof description === "string" ? (
+ {description}
+ ) : (
+
+ )}
+ >
+ )}
+ {buttonLink && (
+
+
+
+
+ )}
+
+
+);
+
+const styles = StyleSheet.create({
+ textCenter: {
+ textAlign: "center"
+ },
+ wizardContent: {
+ ...IOStyles.flex,
+ ...IOStyles.horizontalContentPadding,
+ ...IOStyles.centerJustified
+ }
+});
+
+export { CustomWizardScreen };
diff --git a/ts/navigation/AuthenticationNavigator.tsx b/ts/navigation/AuthenticationNavigator.tsx
index 48644b11715..48876222651 100644
--- a/ts/navigation/AuthenticationNavigator.tsx
+++ b/ts/navigation/AuthenticationNavigator.tsx
@@ -21,6 +21,8 @@ import { AuthSessionPage } from "../screens/authentication/idpAuthSessionHandler
import CieNotSupported from "../components/cie/CieNotSupported";
import RootedDeviceModal from "../screens/modal/RootedDeviceModal";
import { isGestureEnabled } from "../utils/navigation";
+import CieAuthErrorScreen from "../screens/authentication/cie/CieAuthErrorScreen";
+import UnlockAccessScreen from "../screens/authentication/UnlockAccessScreen";
import { AuthenticationParamsList } from "./params/AuthenticationParamsList";
import ROUTES from "./routes";
import CloseButton from "./components/CloseButton";
@@ -121,6 +123,16 @@ const AuthenticationStackNavigator = () => (
component={CieWrongCiePinScreen}
/>
+
+
+
+
{
);
} else if (pot.isError(requestState)) {
if (errorCode === "1002") {
- return ;
+ // TODO: refactor this logic and
+ // change this UnlockAccessComponent with navigation
+ // props.navigation.navigate(ROUTES.AUTHENTICATION, {
+ // screen: ROUTES.UNLOCK_ACCESS_SCREEN,
+ // params: { authLevel: "L2" }
+ // });
+ // jira ticket: https://pagopa.atlassian.net/browse/IOPID-1547
+ return ;
} else {
return (
{
- const { identifier } = props;
+const UnlockAccessComponent = (props: UnlockAccessProps) => {
+ const { authLevel } = props;
const navigation = useIONavigation();
const ModalContent = () => (
@@ -84,67 +81,50 @@ const UnlockAccessScreen = (props: Props) => {
100
);
+ const onPressActionButton = () => {
+ if (authLevel === "L2") {
+ navigation.navigate(ROUTES.AUTHENTICATION, {
+ screen: ROUTES.AUTHENTICATION_LANDING
+ });
+ }
+ // for the future developement: add here
+ // the navigation to continue the flow
+ // future development jira task:
+ // https://pagopa.atlassian.net/browse/IOPID-1228
+ };
+
return (
-
-
+ openWebUrl("https://ioapp.it/")
}}
- secondaryActionProps={{
+ actionButton={{
testID: "button-link-test",
- label: I18n.t("authentication.unlock.loginIO"),
- accessibilityLabel: I18n.t("authentication.unlock.loginIO"),
- onPress: () =>
- navigation.navigate(ROUTES.AUTHENTICATION, {
- screen: ROUTES.AUTHENTICATION_LANDING
- })
+ label:
+ authLevel === "L2"
+ ? I18n.t("global.buttons.close")
+ : I18n.t("authentication.unlock.loginIO"),
+ onPress: onPressActionButton
}}
- >
-
-
-
-
-
-
-
-
- {I18n.t("authentication.unlock.title")}
-
-
-
-
-
- {identifier === "SPID"
- ? I18n.t("authentication.unlock.subtitlel2")
- : I18n.t("authentication.unlock.subtitlel3")}
-
-
-
-
-
-
-
- {veryLongAutoResizableBottomSheetWithFooter}
-
-
-
+ />
+ {veryLongAutoResizableBottomSheetWithFooter}
+ >
);
};
-export default UnlockAccessScreen;
+export default UnlockAccessComponent;
diff --git a/ts/screens/authentication/UnlockAccessScreen.tsx b/ts/screens/authentication/UnlockAccessScreen.tsx
new file mode 100644
index 00000000000..3c5dd983f85
--- /dev/null
+++ b/ts/screens/authentication/UnlockAccessScreen.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import { Route, useRoute } from "@react-navigation/native";
+import ROUTES from "../../navigation/routes";
+import UnlockAccessComponent, {
+ UnlockAccessProps
+} from "./UnlockAccessComponent";
+
+const UnlockAccessScreen = () => {
+ const route =
+ useRoute>();
+ const { authLevel } = route.params;
+
+ return ;
+};
+
+export default UnlockAccessScreen;
diff --git a/ts/screens/authentication/__tests__/UnlockAccessComponent.test.tsx b/ts/screens/authentication/__tests__/UnlockAccessComponent.test.tsx
new file mode 100644
index 00000000000..5f3ff318cb3
--- /dev/null
+++ b/ts/screens/authentication/__tests__/UnlockAccessComponent.test.tsx
@@ -0,0 +1,109 @@
+import React from "react";
+import { render, fireEvent } from "@testing-library/react-native";
+import { View } from "react-native";
+import { createStore } from "redux";
+import UnlockAccessComponent, {
+ UnlockAccessProps
+} from "../../authentication/UnlockAccessComponent";
+import { useIOBottomSheetAutoresizableModal } from "../../../utils/hooks/bottomSheet";
+import { openWebUrl } from "../../../utils/url";
+import { useIONavigation } from "../../../navigation/params/AppParamsList";
+import I18n from "../../../i18n";
+import ROUTES from "../../../navigation/routes";
+import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapper";
+import { appReducer } from "../../../store/reducers";
+import { applicationChangeState } from "../../../store/actions/application";
+
+jest.mock("../../../utils/hooks/bottomSheet");
+jest.mock("../../../utils/url");
+jest.mock("../../../navigation/params/AppParamsList");
+
+const mockedUseIOBottomSheetAutoresizableModal =
+ useIOBottomSheetAutoresizableModal as jest.Mock;
+const mockedOpenWebUrl = openWebUrl as jest.Mock;
+const mockedUseIONavigation = useIONavigation as jest.Mock;
+
+describe("UnlockAccessComponent", () => {
+ const mockNavigation = {
+ navigate: jest.fn()
+ };
+
+ beforeEach(() => {
+ mockedUseIONavigation.mockReturnValue(mockNavigation);
+ mockedUseIOBottomSheetAutoresizableModal.mockReturnValue({
+ present: jest.fn(),
+ bottomSheet: null
+ });
+ jest.clearAllMocks();
+ });
+
+ it("renders correctly with authLevel L2", () => {
+ const { getByText, getAllByText } = renderComponent("L2");
+
+ expect(getAllByText(I18n.t("authentication.unlock.title"))).toHaveLength(2);
+ expect(getByText(I18n.t("authentication.unlock.subtitlel2"))).toBeTruthy();
+ });
+
+ it("renders correctly with authLevel L3", () => {
+ const { getByText, getAllByText } = renderComponent("L3");
+
+ expect(getAllByText(I18n.t("authentication.unlock.title"))).toHaveLength(2);
+ expect(getByText(I18n.t("authentication.unlock.subtitlel3"))).toBeTruthy();
+ });
+
+ it("calls openWebUrl when primary button is pressed", () => {
+ const { getByTestId } = renderComponent("L2");
+ const button = getByTestId("button-solid-test");
+
+ fireEvent.press(button);
+
+ expect(mockedOpenWebUrl).toHaveBeenCalledWith("https://ioapp.it/");
+ });
+
+ it("calls navigation.navigate when action button is pressed with authLevel L2", () => {
+ const { getByTestId } = renderComponent("L2");
+ const button = getByTestId("button-link-test");
+
+ fireEvent.press(button);
+
+ expect(mockNavigation.navigate).toHaveBeenCalledWith(
+ ROUTES.AUTHENTICATION,
+ {
+ screen: ROUTES.AUTHENTICATION_LANDING
+ }
+ );
+ });
+
+ it("calls present modal when learn more link is pressed", () => {
+ const presentMock = jest.fn();
+ const bottomSheetMock = ;
+ mockedUseIOBottomSheetAutoresizableModal.mockReturnValueOnce({
+ present: presentMock,
+ bottomSheet: bottomSheetMock
+ });
+
+ const { getByTestId } = renderComponent("L2");
+ const link = getByTestId("learn-more-link-test");
+
+ fireEvent.press(link);
+
+ expect(presentMock).toHaveBeenCalled();
+ });
+});
+
+const renderComponent = (authLevel?: "L2" | "L3") => {
+ if (authLevel) {
+ const props: UnlockAccessProps = { authLevel };
+ return render();
+ } else {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+ const store = createStore(appReducer, globalState as any);
+
+ return renderScreenWithNavigationStoreContext(
+ UnlockAccessComponent,
+ "DUMMY",
+ {},
+ store
+ );
+ }
+};
diff --git a/ts/screens/authentication/__tests__/UnlockAccessScreen.test.tsx b/ts/screens/authentication/__tests__/UnlockAccessScreen.test.tsx
new file mode 100644
index 00000000000..b4adeeb8c47
--- /dev/null
+++ b/ts/screens/authentication/__tests__/UnlockAccessScreen.test.tsx
@@ -0,0 +1,54 @@
+import configureMockStore from "redux-mock-store";
+import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapper";
+import { appReducer } from "../../../store/reducers";
+import { applicationChangeState } from "../../../store/actions/application";
+import ROUTES from "../../../navigation/routes";
+import { GlobalState } from "../../../store/reducers/types";
+import { UnlockAccessProps } from "../../authentication/UnlockAccessComponent";
+import UnlockAccessScreen from "../../authentication/UnlockAccessScreen";
+import I18n from "../../../i18n";
+
+describe("UnlockAccessScreen", () => {
+ it("render UnlockAccessComponent with authLevel L2", () => {
+ const component = renderComponent({ authLevel: "L2" });
+
+ expect(
+ component.screen.getAllByText(I18n.t("authentication.unlock.title"))
+ ).toHaveLength(2);
+
+ expect(
+ component.screen.getByText(I18n.t("authentication.unlock.subtitlel2"))
+ ).toBeTruthy();
+ });
+
+ it("render UnlockAccessComponent with authLevel L3", () => {
+ const component = renderComponent({ authLevel: "L3" });
+
+ expect(
+ component.screen.getAllByText(I18n.t("authentication.unlock.title"))
+ ).toHaveLength(2);
+ expect(
+ component.screen.getByText(I18n.t("authentication.unlock.subtitlel3"))
+ ).toBeTruthy();
+ });
+});
+
+const renderComponent = (props: UnlockAccessProps) => {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+
+ const mockStore = configureMockStore();
+ const store: ReturnType = mockStore({
+ ...globalState
+ } as GlobalState);
+
+ return {
+ screen: renderScreenWithNavigationStoreContext(
+ UnlockAccessScreen,
+ ROUTES.UNLOCK_ACCESS_SCREEN,
+ {
+ ...props
+ },
+ store
+ )
+ };
+};
diff --git a/ts/screens/authentication/cie/CieAuthErrorScreen.tsx b/ts/screens/authentication/cie/CieAuthErrorScreen.tsx
new file mode 100644
index 00000000000..a8fe15a30c8
--- /dev/null
+++ b/ts/screens/authentication/cie/CieAuthErrorScreen.tsx
@@ -0,0 +1,39 @@
+import { Route, useRoute } from "@react-navigation/native";
+import React, { useCallback } from "react";
+import { useIONavigation } from "../../../navigation/params/AppParamsList";
+import ROUTES from "../../../navigation/routes";
+import AuthErrorScreen from "./components/AuthErrorScreen";
+
+export type AuthErrorScreenProps = {
+ errorCode?: string;
+};
+
+const CieAuthErrorScreen = () => {
+ const route =
+ useRoute>();
+ const { errorCode } = route.params;
+
+ const navigation = useIONavigation();
+
+ const onRetry = useCallback(() => {
+ navigation.navigate(ROUTES.AUTHENTICATION, {
+ screen: ROUTES.CIE_PIN_SCREEN
+ });
+ }, [navigation]);
+
+ const onCancel = useCallback(() => {
+ navigation.navigate(ROUTES.AUTHENTICATION, {
+ screen: ROUTES.AUTHENTICATION_LANDING
+ });
+ }, [navigation]);
+
+ return (
+
+ );
+};
+
+export default CieAuthErrorScreen;
diff --git a/ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx b/ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx
index 9dc9877ef5e..65b9d7854e2 100644
--- a/ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx
+++ b/ts/screens/authentication/cie/CieConsentDataUsageScreen.tsx
@@ -2,230 +2,175 @@
* A screen to display, by a webview, the consent to send user sensitive data
* to backend and proceed with the onboarding process
*/
-import * as React from "react";
-import { Alert, BackHandler, NativeEventSubscription } from "react-native";
-import WebView from "react-native-webview";
+import React, { useCallback, useEffect, useState } from "react";
import {
WebViewHttpErrorEvent,
WebViewNavigation
} from "react-native-webview/lib/WebViewTypes";
-import { connect } from "react-redux";
import { VSpacer } from "@pagopa/io-app-design-system";
import { Route, useRoute } from "@react-navigation/native";
-import LoadingSpinnerOverlay from "../../../components/LoadingSpinnerOverlay";
-import GenericErrorComponent from "../../../components/screens/GenericErrorComponent";
-import TopScreenComponent from "../../../components/screens/TopScreenComponent";
-import I18n from "../../../i18n";
+import WebView from "react-native-webview";
+import { useIODispatch } from "../../../store/hooks";
import {
loginFailure,
loginSuccess
} from "../../../store/actions/authentication";
-import { resetToAuthenticationRoute } from "../../../store/actions/navigation";
-import { Dispatch } from "../../../store/actions/types";
import { SessionToken } from "../../../types/SessionToken";
import { onLoginUriChanged } from "../../../utils/login";
+import LoadingSpinnerOverlay from "../../../components/LoadingSpinnerOverlay";
+import { trackLoginCieDataSharingError } from "../analytics/cieAnalytics";
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";
-import {
- trackLoginCieConsentDataUsageScreen,
- trackLoginCieDataSharingError
-} from "../analytics/cieAnalytics";
+import ROUTES from "../../../navigation/routes";
+import { useIONavigation } from "../../../navigation/params/AppParamsList";
+import { useOnboardingAbortAlert } from "../../../utils/hooks/useOnboardingAbortAlert";
+import { useHardwareBackButton } from "../../../hooks/useHardwareBackButton";
export type CieConsentDataUsageScreenNavigationParams = {
cieConsentUri: string;
+ errorCodeDebugMode?: string;
};
-type State = {
- hasError: boolean;
- errorCode?: string;
- isLoginSuccess?: boolean;
-};
-
-type Props = ReturnType &
- ReturnType;
-
-const loaderComponent = (
+const LoaderComponent = () => (
);
-type CieConsentDataUsageScreenProps = Props &
- CieConsentDataUsageScreenNavigationParams;
-class CieConsentDataUsageScreen extends React.Component<
- CieConsentDataUsageScreenProps,
- State
-> {
- private subscription: NativeEventSubscription | undefined;
- constructor(props: CieConsentDataUsageScreenProps) {
- super(props);
- trackLoginCieConsentDataUsageScreen();
- this.state = {
- hasError: false,
- isLoginSuccess: undefined
- };
- }
+const CieConsentDataUsageScreen = () => {
+ const route =
+ useRoute<
+ Route<
+ typeof ROUTES.CIE_CONSENT_DATA_USAGE,
+ CieConsentDataUsageScreenNavigationParams
+ >
+ >();
+ const { cieConsentUri } = route.params;
+ const dispatch = useIODispatch();
+ const [hasError, setHasError] = useState(false);
+ const [isLoginSuccess, setIsLoginSuccess] = useState();
+ const [errorCode, setErrorCode] = useState();
+ const { showAlert } = useOnboardingAbortAlert();
+ const navigation = useIONavigation();
+ const loginSuccessDispatch = useCallback(
+ (token: SessionToken) => dispatch(loginSuccess({ token, idp: "cie" })),
+ [dispatch]
+ );
+
+ const loginFailureDispatch = useCallback(
+ (error: Error) => dispatch(loginFailure({ error, idp: "cie" })),
+ [dispatch]
+ );
+
+ const navigateToLandingScreen = useCallback(() => {
+ navigation.navigate(ROUTES.AUTHENTICATION, {
+ screen: ROUTES.AUTHENTICATION_LANDING
+ });
+ }, [navigation]);
- private showAbortAlert = (): boolean => {
+ const showAbortAlert = useCallback((): boolean => {
// if the screen is in error state, skip the confirmation alert to go back at the landing screen
- if (this.state.hasError) {
- this.props.resetNavigation();
+ if (hasError) {
+ navigateToLandingScreen();
return true;
}
- Alert.alert(
- I18n.t("onboarding.alert.title"),
- I18n.t("onboarding.alert.description"),
- [
- {
- text: I18n.t("global.buttons.cancel"),
- style: "cancel"
- },
- {
- text: I18n.t("global.buttons.exit"),
- style: "default",
- onPress: this.props.resetNavigation
- }
- ]
- );
+ showAlert();
return true;
- };
-
- public componentDidMount() {
- // eslint-disable-next-line functional/immutable-data
- this.subscription = BackHandler.addEventListener(
- "hardwareBackPress",
- this.showAbortAlert
- );
- }
-
- public componentWillUnmount() {
- this.subscription?.remove();
- }
+ }, [hasError, navigateToLandingScreen, showAlert]);
- get cieAuthorizationUri(): string {
- return this.props.cieConsentUri;
- }
+ useHardwareBackButton(() => {
+ showAbortAlert();
+ return true;
+ });
- private handleWebViewError = () => {
- this.setState({ hasError: true });
- };
+ const handleWebViewError = useCallback(() => setHasError(true), []);
- private handleHttpError = (event: WebViewHttpErrorEvent) => {
- this.props.loginFailure(
- new Error(
- `HTTP error ${event.nativeEvent.description} with Authorization uri`
- )
- );
- };
-
- private handleLoginSuccess = (token: SessionToken) => {
- this.setState({ isLoginSuccess: true, hasError: false }, () => {
- this.props.loginSuccess(token);
- });
- };
-
- private handleShouldStartLoading = (event: WebViewNavigation): boolean => {
- const isLoginUrlWithToken = onLoginUriChanged(
- this.handleLoginFailure,
- this.handleLoginSuccess
- )(event);
- // URL can be loaded if it's not the login URL containing the session token - this avoids
- // making a (useless) GET request with the session in the URL
- return !isLoginUrlWithToken;
- };
-
- private handleLoginFailure = (errorCode?: string) => {
- this.props.loginFailure(
- new Error(`login CIE failure with code ${errorCode || "n/a"}`)
- );
- this.setState({ hasError: true, errorCode });
- };
-
- private getContent = () => {
- if (this.state.isLoginSuccess) {
- return loaderComponent;
+ const handleHttpError = useCallback(
+ (event: WebViewHttpErrorEvent) => {
+ loginFailureDispatch(
+ new Error(
+ `HTTP error ${event.nativeEvent.description} with Authorization uri`
+ )
+ );
+ },
+ [loginFailureDispatch]
+ );
+
+ const handleLoginSuccess = useCallback(
+ (token: SessionToken) => {
+ setIsLoginSuccess(true);
+ setHasError(false);
+ loginSuccessDispatch(token);
+ },
+ [loginSuccessDispatch]
+ );
+
+ const handleLoginFailure = useCallback(
+ (errorCode?: string) => {
+ setHasError(true);
+ setErrorCode(errorCode);
+ loginFailureDispatch(
+ new Error(`login CIE failure with code ${errorCode || "n/a"}`)
+ );
+ },
+ [loginFailureDispatch]
+ );
+
+ const handleShouldStartLoading = useCallback(
+ (event: WebViewNavigation): boolean => {
+ const isLoginUrlWithToken = onLoginUriChanged(
+ handleLoginFailure,
+ handleLoginSuccess
+ )(event);
+ // URL can be loaded if it's not the login URL containing the session token - this avoids
+ // making a (useless) GET request with the session in the URL
+ return !isLoginUrlWithToken;
+ },
+ [handleLoginFailure, handleLoginSuccess]
+ );
+
+ useEffect(() => {
+ if (hasError && errorCode === "22") {
+ trackLoginCieDataSharingError();
}
- if (this.state.hasError) {
- if (this.state.errorCode === "22") {
- trackLoginCieDataSharingError();
+ }, [errorCode, hasError]);
+
+ useEffect(() => {
+ if (hasError) {
+ if (errorCode === "1002") {
+ navigation.navigate(ROUTES.AUTHENTICATION, {
+ screen: ROUTES.UNLOCK_ACCESS_SCREEN,
+ params: { authLevel: "L2" }
+ });
+ return;
}
- if (this.state.errorCode === "1002") {
- return ;
- } else {
- const errorTranslationKey = this.state.errorCode
- ? `authentication.errors.spid.error_${this.state.errorCode}`
- : "authentication.errors.network.title";
- return (
-
- );
- }
- } else {
- return (
- loaderComponent}
- onError={this.handleWebViewError}
- onHttpError={this.handleHttpError}
- />
- );
+ navigation.navigate(ROUTES.AUTHENTICATION, {
+ screen: ROUTES.AUTH_ERROR_SCREEN,
+ params: { errorCode }
+ });
}
- };
+ }, [errorCode, hasError, navigation]);
- public render(): React.ReactNode {
- const goBack = this.state.hasError ? false : this.showAbortAlert;
+ if (isLoginSuccess) {
+ return ;
+ }
+ if (!hasError) {
return (
-
- {this.getContent()}
-
+ }
+ onError={handleWebViewError}
+ onHttpError={handleHttpError}
+ />
);
}
-}
-
-const mapStateToProps = (state: GlobalState) => ({
- isCieUatEnabled: isCieLoginUatEnabledSelector(state)
-});
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
- resetNavigation: () => resetToAuthenticationRoute(),
- loginSuccess: (token: SessionToken) =>
- dispatch(loginSuccess({ token, idp: "cie" })),
- loginFailure: (error: Error) => dispatch(loginFailure({ error, idp: "cie" }))
-});
-
-const CieConsentDataUsageScreenFC = (props: Props) => {
- const { cieConsentUri } =
- useRoute<
- Route<"CIE_CONSENT_DATA_USAGE", CieConsentDataUsageScreenNavigationParams>
- >().params;
- return ;
+ return null;
};
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(CieConsentDataUsageScreenFC);
+export default CieConsentDataUsageScreen;
diff --git a/ts/screens/authentication/cie/__test__/AuthErrorScreen.test.tsx b/ts/screens/authentication/cie/__test__/AuthErrorScreen.test.tsx
new file mode 100644
index 00000000000..4943d8a75b1
--- /dev/null
+++ b/ts/screens/authentication/cie/__test__/AuthErrorScreen.test.tsx
@@ -0,0 +1,70 @@
+import React from "react";
+import { fireEvent, render } from "@testing-library/react-native";
+import AuthErrorScreen from "../components/AuthErrorScreen";
+import I18n from "../../../../i18n";
+
+describe("AuthErrorScreen", () => {
+ const testCases = [
+ {
+ description: "renders correctly with generic error code",
+ errorCode: "generic",
+ expectedTitle: I18n.t("authentication.cie_errors.generic.title"),
+ expectedSubtitle: I18n.t("authentication.cie_errors.generic.subtitle"),
+ expectRetryCalled: true,
+ expectCancelCalled: true
+ },
+ {
+ description: 'renders correctly with error code "22"',
+ errorCode: "22",
+ expectedTitle: I18n.t("authentication.cie_errors.error_22.title"),
+ expectedSubtitle: I18n.t("authentication.cie_errors.error_22.subtitle"),
+ expectRetryCalled: true,
+ expectCancelCalled: true
+ },
+ {
+ description: 'renders correctly with error code "1001"',
+ errorCode: "1001",
+ expectedTitle: I18n.t("authentication.cie_errors.error_1001.title"),
+ expectedSubtitle: I18n.t("authentication.cie_errors.error_1001.subtitle"),
+ expectRetryCalled: false,
+ expectCancelCalled: true
+ }
+ ];
+
+ testCases.forEach(
+ ({
+ description,
+ errorCode,
+ expectedTitle,
+ expectedSubtitle,
+ expectRetryCalled,
+ expectCancelCalled
+ }) => {
+ test(description, () => {
+ const onRetryMock = jest.fn();
+ const onCancelMock = jest.fn();
+
+ const { getByText } = render(
+
+ );
+
+ expect(getByText(expectedTitle)).toBeDefined();
+ expect(getByText(expectedSubtitle)).toBeDefined();
+
+ if (expectRetryCalled) {
+ fireEvent.press(getByText(I18n.t("global.buttons.retry")));
+ expect(onRetryMock).toHaveBeenCalled();
+ }
+
+ if (expectCancelCalled) {
+ fireEvent.press(getByText(I18n.t("global.buttons.close")));
+ expect(onCancelMock).toHaveBeenCalled();
+ }
+ });
+ }
+ );
+});
diff --git a/ts/screens/authentication/cie/components/AuthErrorScreen.tsx b/ts/screens/authentication/cie/components/AuthErrorScreen.tsx
new file mode 100644
index 00000000000..55e0ee4e444
--- /dev/null
+++ b/ts/screens/authentication/cie/components/AuthErrorScreen.tsx
@@ -0,0 +1,65 @@
+/* eslint-disable no-console */
+import React from "react";
+import {
+ OperationResultScreenContent,
+ OperationResultScreenContentProps
+} from "../../../../components/screens/OperationResultScreenContent";
+import I18n from "../../../../i18n";
+
+type Props = {
+ errorCode?: string;
+ onRetry: () => void;
+ onCancel: () => void;
+};
+
+const AuthErrorScreen = ({
+ errorCode = "generic",
+ onRetry,
+ onCancel
+}: Props) => {
+ const errorsObject: {
+ [key: string]: OperationResultScreenContentProps;
+ } = {
+ "22": {
+ pictogram: "accessDenied",
+ title: I18n.t("authentication.cie_errors.error_22.title"),
+ subtitle: I18n.t("authentication.cie_errors.error_22.subtitle"),
+ action: {
+ onPress: onRetry,
+ label: I18n.t("global.buttons.retry")
+ },
+ secondaryAction: {
+ onPress: onCancel,
+ label: I18n.t("global.buttons.close")
+ }
+ },
+ "1001": {
+ pictogram: "identityCheck",
+ title: I18n.t("authentication.cie_errors.error_1001.title"),
+ subtitle: I18n.t("authentication.cie_errors.error_1001.subtitle"),
+ action: {
+ onPress: onCancel,
+ label: I18n.t("global.buttons.close")
+ }
+ },
+ generic: {
+ pictogram: "umbrellaNew",
+ title: I18n.t("authentication.cie_errors.generic.title"),
+ subtitle: I18n.t("authentication.cie_errors.generic.subtitle"),
+ action: {
+ onPress: onRetry,
+ label: I18n.t("global.buttons.retry")
+ },
+ secondaryAction: {
+ onPress: onCancel,
+ label: I18n.t("global.buttons.close")
+ }
+ }
+ };
+
+ const errorDetails = errorsObject[errorCode] || errorsObject.generic;
+
+ return ;
+};
+
+export default AuthErrorScreen;
diff --git a/ts/screens/onboarding/__tests__/UnlockAccessScreen.test.tsx b/ts/screens/onboarding/__tests__/UnlockAccessScreen.test.tsx
deleted file mode 100644
index 2c26211f0dd..00000000000
--- a/ts/screens/onboarding/__tests__/UnlockAccessScreen.test.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-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
- );
-};
From 439e7b0885f8fe01b9d26d36701fb8ae36e5dd28 Mon Sep 17 00:00:00 2001
From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com>
Date: Wed, 22 May 2024 17:36:55 +0200
Subject: [PATCH 2/5] chore(release): 2.61.0-rc.0
---
CHANGELOG.md | 28 +++++++++++++++++++++++++
android/app/build.gradle | 4 ++--
ios/ItaliaApp.xcodeproj/project.pbxproj | 4 ++--
ios/ItaliaApp/Info.plist | 4 ++--
ios/ItaliaAppTests/Info.plist | 4 ++--
package.json | 2 +-
publiccode.yml | 4 ++--
7 files changed, 39 insertions(+), 11 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ed6cfdaa26..712be5972e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,34 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+## [2.61.0-rc.0](https://github.com/pagopa/io-app/compare/2.60.0-rc.2...2.61.0-rc.0) (2024-05-22)
+
+
+### Features
+
+* [[IOCOM-1391](https://pagopa.atlassian.net/browse/IOCOM-1391)] Flag to enable Messages Home with the new DS ([#5766](https://github.com/pagopa/io-app/issues/5766)) ([fdca533](https://github.com/pagopa/io-app/commit/fdca533b583000ecd4c066ba0ca17f8a74e3786e))
+* [[IOCOM-823](https://pagopa.atlassian.net/browse/IOCOM-823)] Chips for Inbox and Archived messages on new DS Messages' Home ([#5770](https://github.com/pagopa/io-app/issues/5770)) ([cb333d9](https://github.com/pagopa/io-app/commit/cb333d920955eaf96124f1b054ca2d53f8ef072e))
+* [[IOPID-1548](https://pagopa.atlassian.net/browse/IOPID-1548)] DS add new cie errors ([#5750](https://github.com/pagopa/io-app/issues/5750)) ([26916b7](https://github.com/pagopa/io-app/commit/26916b76f9c867a354613d4d6b48543ace4bcb8f))
+* [[IOPID-1725](https://pagopa.atlassian.net/browse/IOPID-1725)] New DS on CIE certificates KO screen ([#5775](https://github.com/pagopa/io-app/issues/5775)) ([26db96b](https://github.com/pagopa/io-app/commit/26db96bf693acc46a4f14faab3f4040a9bb95969)), closes [/github.com/pagopa/io-app/blob/d5d5681817064085219e3854e4fef56431c4519b/ts/screens/authentication/cie/CieCardReaderScreen.tsx#L282](https://github.com/pagopa//github.com/pagopa/io-app/blob/d5d5681817064085219e3854e4fef56431c4519b/ts/screens/authentication/cie/CieCardReaderScreen.tsx/issues/L282) [/github.com/pagopa/io-app/blob/d5d5681817064085219e3854e4fef56431c4519b/ts/screens/authentication/cie/CieCardReaderScreen.tsx#L298](https://github.com/pagopa//github.com/pagopa/io-app/blob/d5d5681817064085219e3854e4fef56431c4519b/ts/screens/authentication/cie/CieCardReaderScreen.tsx/issues/L298)
+* **IT Wallet:** [[SIW-980](https://pagopa.atlassian.net/browse/SIW-980)] Add eID issuing IDP selection screen ([#5752](https://github.com/pagopa/io-app/issues/5752)) ([d5d5681](https://github.com/pagopa/io-app/commit/d5d5681817064085219e3854e4fef56431c4519b))
+
+
+### Bug Fixes
+
+* [[IOPAE-1129](https://pagopa.atlassian.net/browse/IOPAE-1129)] Service Details bottom gradient displayed only with CTAs ([#5781](https://github.com/pagopa/io-app/issues/5781)) ([8164a03](https://github.com/pagopa/io-app/commit/8164a036eec31c81b750dfd15aa5cc93f003d0da))
+* [[IOPAE-1164](https://pagopa.atlassian.net/browse/IOPAE-1164)] Fix infinite scroll in `InstitutionServicesScreen` ([#5774](https://github.com/pagopa/io-app/issues/5774)) ([8760edc](https://github.com/pagopa/io-app/commit/8760edc9242e945baf027dd4410fd3945a5a473b))
+* `LandingScreen`'s `Carousel` translations ([#5789](https://github.com/pagopa/io-app/issues/5789)) ([04c1105](https://github.com/pagopa/io-app/commit/04c11053cd55ce2b2ae394066c80f5cfa7b93877))
+
+
+### Chores
+
+* [[IOBP-575](https://pagopa.atlassian.net/browse/IOBP-575)] Removed seconds from a11y label transaction detail date ([#5786](https://github.com/pagopa/io-app/issues/5786)) ([a4e1f79](https://github.com/pagopa/io-app/commit/a4e1f7930f49aca62a31353ea5a9ff163577aa48))
+* **IT Wallet:** [[SIW-1127](https://pagopa.atlassian.net/browse/SIW-1127)] IT Wallet POC utils implementation ([#5779](https://github.com/pagopa/io-app/issues/5779)) ([6610236](https://github.com/pagopa/io-app/commit/6610236817163bea59b303ee056554223b9a7860))
+* **IT Wallet:** [[SIW-1128](https://pagopa.atlassian.net/browse/SIW-1128)] Add `EidCard` and `EidCardPreview` components ([#5772](https://github.com/pagopa/io-app/issues/5772)) ([8d55432](https://github.com/pagopa/io-app/commit/8d55432dcf081d617a89a6d42cc9ed4dd906f32b))
+* [[IOPAE-1145](https://pagopa.atlassian.net/browse/IOPAE-1145)] Add FeaturedInstitution and FeaturedService carousel/card layout ([#5771](https://github.com/pagopa/io-app/issues/5771)) ([2b85bf7](https://github.com/pagopa/io-app/commit/2b85bf750ba18defa8bc8409b66ab1d738f43604))
+* **Cross:** [[IOAPPX-283](https://pagopa.atlassian.net/browse/IOAPPX-283)] Add `IOScrollView` (next iteration of `GradientScroll`, now deprecated) + `IOScrollViewWithLargeHeader` ([#5704](https://github.com/pagopa/io-app/issues/5704)) ([b4f2762](https://github.com/pagopa/io-app/commit/b4f276279a0384e824221e2ba50651f7020f268a))
+* **Cross:** [[IOAPPX-295](https://pagopa.atlassian.net/browse/IOAPPX-295)] Remove nested navigator from DS section ([#5763](https://github.com/pagopa/io-app/issues/5763)) ([55fa198](https://github.com/pagopa/io-app/commit/55fa198da99e3fda5aaefd7a9e689eab8ab59370))
+
## [2.60.0-rc.2](https://github.com/pagopa/io-app/compare/2.60.0-rc.1...2.60.0-rc.2) (2024-05-15)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 23701945408..eea73188746 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -145,8 +145,8 @@ android {
applicationId "it.pagopa.io.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 100154816
- versionName "2.60.0.2"
+ versionCode 100154817
+ versionName "2.61.0.0"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) {
// We configure the CMake build only if you decide to opt-in for the New Architecture.
diff --git a/ios/ItaliaApp.xcodeproj/project.pbxproj b/ios/ItaliaApp.xcodeproj/project.pbxproj
index fb0b9806791..3d1b54b6d05 100644
--- a/ios/ItaliaApp.xcodeproj/project.pbxproj
+++ b/ios/ItaliaApp.xcodeproj/project.pbxproj
@@ -784,7 +784,7 @@
CODE_SIGN_ENTITLEMENTS = ItaliaApp/ItaliaApp.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- CURRENT_PROJECT_VERSION = 2;
+ CURRENT_PROJECT_VERSION = 0;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = M2X5YQ4BJ7;
ENABLE_BITCODE = NO;
@@ -825,7 +825,7 @@
CODE_SIGN_ENTITLEMENTS = ItaliaApp/ItaliaApp.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
- CURRENT_PROJECT_VERSION = 2;
+ CURRENT_PROJECT_VERSION = 0;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = M2X5YQ4BJ7;
ENABLE_BITCODE = NO;
diff --git a/ios/ItaliaApp/Info.plist b/ios/ItaliaApp/Info.plist
index c5300077562..c512487335d 100644
--- a/ios/ItaliaApp/Info.plist
+++ b/ios/ItaliaApp/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2.60.0
+ 2.61.0
CFBundleSignature
????
CFBundleURLTypes
@@ -34,7 +34,7 @@
CFBundleVersion
- 2
+ 0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/ItaliaAppTests/Info.plist b/ios/ItaliaAppTests/Info.plist
index 76cc1a0a17a..9ece5203d9b 100644
--- a/ios/ItaliaAppTests/Info.plist
+++ b/ios/ItaliaAppTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 2.60.0
+ 2.61.0
CFBundleSignature
????
CFBundleVersion
- 2
+ 0
\ No newline at end of file
diff --git a/package.json b/package.json
index 415e28cbf78..fc1ca314e3f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "italia-app",
- "version": "2.60.0-rc.2",
+ "version": "2.61.0-rc.0",
"io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.32.1-RELEASE/api_backend.yaml",
"io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.32.1-RELEASE/api_public.yaml",
"io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.31/definitions.yml",
diff --git a/publiccode.yml b/publiccode.yml
index fa15927aa23..be014de13ed 100644
--- a/publiccode.yml
+++ b/publiccode.yml
@@ -5,11 +5,11 @@
publiccodeYmlVersion: '0.2'
name: IO
logo: "img/app-logo.svg"
-releaseDate: '2024-05-15'
+releaseDate: '2024-05-22'
url: 'https://github.com/pagopa/io-app'
applicationSuite: IO
landingURL: 'https://io.italia.it/'
-softwareVersion: 2.60.0-rc.2
+softwareVersion: 2.61.0-rc.0
developmentStatus: beta
softwareType: standalone/mobile
roadmap: 'https://io.italia.it/'
From 3bfb1d2df9435ca2c439d289290c98751dbbfd03 Mon Sep 17 00:00:00 2001
From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com>
Date: Thu, 23 May 2024 08:45:43 +0200
Subject: [PATCH 3/5] chore: change EIC login flow with the dev server to
navigate to `CieConsentDataUsageScreen` (#5788)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Short description
This PR changes the EIC login flow with the dev server to navigate to
`CieConsentDataUsageScreen`.
Details
| 🤖 | 🍏 |
| - | - |
| |
## How to test
Set `CIE_LOGIN_WITH_DEV_SERVER_ENABLED=YES` and run the app against the
dev server. Try a login with EIC. You should see a first screen to login
as in the SPID flow. Tapping "Login" you should navigate to
`CieConsentDataUsageScreen` where you see again the screen to login, but
there you can test all the login errors (ex. 22, 25, etc.).
Co-authored-by: Alice Di Rico <83651704+Ladirico@users.noreply.github.com>
---
ts/screens/authentication/cie/CiePinScreen.tsx | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/ts/screens/authentication/cie/CiePinScreen.tsx b/ts/screens/authentication/cie/CiePinScreen.tsx
index e39ca2a240a..76564d69fd5 100644
--- a/ts/screens/authentication/cie/CiePinScreen.tsx
+++ b/ts/screens/authentication/cie/CiePinScreen.tsx
@@ -27,7 +27,10 @@ import {
import { SafeAreaView } from "react-native-safe-area-context";
import { useSelector } from "react-redux";
import { IdpData } from "../../../../definitions/content/IdpData";
-import { CieRequestAuthenticationOverlay } from "../../../components/cie/CieRequestAuthenticationOverlay";
+import {
+ CieEntityIds,
+ CieRequestAuthenticationOverlay
+} from "../../../components/cie/CieRequestAuthenticationOverlay";
import { ContextualHelpPropsMarkdown } from "../../../components/screens/BaseScreenComponent";
import {
BottomTopAnimation,
@@ -56,6 +59,7 @@ import {
trackLoginCiePinInfo,
trackLoginCiePinScreen
} from "../analytics/cieAnalytics";
+import { getIdpLoginUri } from "../../../utils/login";
const CIE_PIN_LENGTH = 8;
@@ -123,8 +127,10 @@ const CiePinScreen = () => {
useEffect(() => {
if (authUrlGenerated !== undefined) {
if (cieFlowForDevServerEnabled) {
- const token = /token=([\d\w]+)/.exec(authUrlGenerated)?.[1];
- doLoginSuccess(token as SessionToken, "cie");
+ const loginUri = getIdpLoginUri(CieEntityIds.PROD, 3);
+ navigation.navigate(ROUTES.CIE_CONSENT_DATA_USAGE, {
+ cieConsentUri: loginUri
+ });
} else {
navigation.navigate(ROUTES.CIE_CARD_READER_SCREEN, {
ciePin: pin,
From ca2de7835f5dcc32080b4568f29682bd523f0069 Mon Sep 17 00:00:00 2001
From: Alessandro Dell'Oste
Date: Thu, 23 May 2024 17:08:46 +0200
Subject: [PATCH 4/5] chore: [IOPAE-1142] Added featured carousels in
`ServicesHomeScreen` (#5793)
## Short description
This PR refactors the `ServicesHomeScreen` screen by adding the featured
services and the featured institutions carousels.
Details
| New services tab |
| - |
| |
## List of changes proposed in this pull request
- Added `CardPressableBase` component
- Updated carousels by adding the `CardPressableBase` component
- Updated `ServicesHomeScreen` screen to display the national
institutions and featured carousels
- Updated locales
## How to test
Using `io-dev-api-server`, navigate to the services tab. Check that the
screen is displayed correctly
---
locales/en/index.yml | 5 +
locales/it/index.yml | 5 +
.../common/components/CardPressableBase.tsx | 35 +
.../components/InstitutionListSkeleton.tsx} | 18 +
.../components/FeaturedInstitutionCard.tsx | 53 +-
.../components/FeaturedInstitutionList.tsx | 76 ++
.../home/components/FeaturedServiceCard.tsx | 66 +-
.../home/components/FeaturedServiceList.tsx | 72 ++
.../components/ServicesHomeIntitutionList.tsx | 123 --
.../FeaturedInstitutionCard.test.tsx | 58 +
.../__tests__/FeaturedServiceCard.test.tsx | 58 +
.../FeaturedInstitutionCard.test.tsx.snap | 990 ++++++++++++++++
.../FeaturedServiceCard.test.tsx.snap | 1024 +++++++++++++++++
.../home/hooks/useInstitutionsFetcher.tsx | 53 +-
.../home/screens/ServicesHomeScreen.tsx | 143 ++-
.../store/reducers/__tests__/store.test.ts | 108 +-
.../services/home/store/reducers/index.ts | 26 +-
17 files changed, 2610 insertions(+), 303 deletions(-)
create mode 100644 ts/features/services/common/components/CardPressableBase.tsx
rename ts/features/services/{home/components/InstitutionListItemSkeleton.tsx => common/components/InstitutionListSkeleton.tsx} (65%)
create mode 100644 ts/features/services/home/components/FeaturedInstitutionList.tsx
create mode 100644 ts/features/services/home/components/FeaturedServiceList.tsx
delete mode 100644 ts/features/services/home/components/ServicesHomeIntitutionList.tsx
create mode 100644 ts/features/services/home/components/__tests__/FeaturedInstitutionCard.test.tsx
create mode 100644 ts/features/services/home/components/__tests__/FeaturedServiceCard.test.tsx
create mode 100644 ts/features/services/home/components/__tests__/__snapshots__/FeaturedInstitutionCard.test.tsx.snap
create mode 100644 ts/features/services/home/components/__tests__/__snapshots__/FeaturedServiceCard.test.tsx.snap
diff --git a/locales/en/index.yml b/locales/en/index.yml
index 18c70b86d43..b500e69f96b 100644
--- a/locales/en/index.yml
+++ b/locales/en/index.yml
@@ -2104,6 +2104,11 @@ services:
emptyListMessage: There are no services available at this time, pull down to refresh
new: New
home:
+ featured:
+ services:
+ title: Featured
+ institutions:
+ title: Featured Institutions
institutions:
title: National
institution:
diff --git a/locales/it/index.yml b/locales/it/index.yml
index af7f473f317..3afe881a8c0 100644
--- a/locales/it/index.yml
+++ b/locales/it/index.yml
@@ -2104,6 +2104,11 @@ services:
emptyListMessage: Non ci sono servizi disponibili al momento, trascina in basso per aggiornare
new: Nuovo
home:
+ featured:
+ services:
+ title: In primo piano
+ institutions:
+ title: Enti in evidenza
institutions:
title: Nazionali
institution:
diff --git a/ts/features/services/common/components/CardPressableBase.tsx b/ts/features/services/common/components/CardPressableBase.tsx
new file mode 100644
index 00000000000..f1ae663d5d8
--- /dev/null
+++ b/ts/features/services/common/components/CardPressableBase.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { Pressable } from "react-native";
+import Animated from "react-native-reanimated";
+import { PressableBaseProps, WithTestID } from "@pagopa/io-app-design-system";
+import { useSpringPressScaleAnimation } from "../../../../components/ui/utils/hooks/useSpringPressScaleAnimation";
+
+type CardPressableBaseProps = WithTestID;
+
+export const CardPressableBase = ({
+ onPress,
+ testID,
+ accessibilityLabel,
+ children
+}: React.PropsWithChildren) => {
+ const { onPressIn, onPressOut, animatedScaleStyle } =
+ useSpringPressScaleAnimation();
+
+ if (onPress === undefined) {
+ return <>{children}>;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/ts/features/services/home/components/InstitutionListItemSkeleton.tsx b/ts/features/services/common/components/InstitutionListSkeleton.tsx
similarity index 65%
rename from ts/features/services/home/components/InstitutionListItemSkeleton.tsx
rename to ts/features/services/common/components/InstitutionListSkeleton.tsx
index cbc8765ebf2..ca9c46203a6 100644
--- a/ts/features/services/home/components/InstitutionListItemSkeleton.tsx
+++ b/ts/features/services/common/components/InstitutionListSkeleton.tsx
@@ -1,6 +1,7 @@
import React from "react";
import { View } from "react-native";
import {
+ Divider,
IOListItemStyles,
IOListItemVisualParams,
IOStyles,
@@ -25,3 +26,20 @@ export const InstitutionListItemSkeleton = () => (
);
+
+type InstitutionListSkeletonProps = {
+ size?: number;
+};
+
+export const InstitutionListSkeleton = ({
+ size = 3
+}: InstitutionListSkeletonProps) => (
+
+ {Array.from({ length: size }).map((_, index) => (
+
+
+ {index < size - 1 ? : undefined}
+
+ ))}
+
+);
diff --git a/ts/features/services/home/components/FeaturedInstitutionCard.tsx b/ts/features/services/home/components/FeaturedInstitutionCard.tsx
index 025127a1966..f2bb41c542d 100644
--- a/ts/features/services/home/components/FeaturedInstitutionCard.tsx
+++ b/ts/features/services/home/components/FeaturedInstitutionCard.tsx
@@ -1,3 +1,6 @@
+import React from "react";
+import { Dimensions, StyleSheet, View } from "react-native";
+import Placeholder from "rn-placeholder";
import {
Avatar,
H6,
@@ -7,18 +10,16 @@ import {
VSpacer
} from "@pagopa/io-app-design-system";
import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings";
-import React from "react";
-import { Dimensions, StyleSheet, View } from "react-native";
-import Placeholder from "rn-placeholder";
import { WithTestID } from "../../../../types/WithTestID";
import { logoForInstitution } from "../utils";
+import { CardPressableBase } from "../../common/components/CardPressableBase";
export type FeaturedInstitutionCardProps = WithTestID<{
id: string;
name: string;
accessibilityLabel?: string;
isNew?: boolean;
- onPress?: (id: string) => void;
+ onPress?: () => void;
}>;
export const CARD_WIDTH =
@@ -59,28 +60,34 @@ const styles = StyleSheet.create({
});
const FeaturedInstitutionCard = (props: FeaturedInstitutionCardProps) => (
-
-
-
-
-
-
-
- {props.name}
-
+
+
+
+
+
+
+
+ {props.name}
+
+
-
+
);
const FeaturedInstitutionCardSkeleton = ({ testID }: WithTestID) => (
diff --git a/ts/features/services/home/components/FeaturedInstitutionList.tsx b/ts/features/services/home/components/FeaturedInstitutionList.tsx
new file mode 100644
index 00000000000..3cd8581f665
--- /dev/null
+++ b/ts/features/services/home/components/FeaturedInstitutionList.tsx
@@ -0,0 +1,76 @@
+import React, { useCallback, useMemo } from "react";
+import { ListItemHeader, VSpacer } from "@pagopa/io-app-design-system";
+import { Institution } from "../../../../../definitions/services/Institution";
+import I18n from "../../../../i18n";
+import { useIONavigation } from "../../../../navigation/params/AppParamsList";
+import { useIODispatch, useIOSelector } from "../../../../store/hooks";
+import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender";
+import { SERVICES_ROUTES } from "../../common/navigation/routes";
+import { featuredInstitutionsGet } from "../store/actions";
+import {
+ featuredInstitutionsSelector,
+ isErrorFeaturedInstitutionsSelector,
+ isLoadingFeaturedInstitutionsSelector
+} from "../store/reducers";
+import {
+ FeaturedInstitutionsCarousel,
+ FeaturedInstitutionsCarouselSkeleton
+} from "./FeaturedInstitutionsCarousel";
+
+export const FeaturedInstitutionList = () => {
+ const dispatch = useIODispatch();
+ const navigation = useIONavigation();
+
+ const featuredInstitutions = useIOSelector(featuredInstitutionsSelector);
+ const isError = useIOSelector(isErrorFeaturedInstitutionsSelector);
+ const isLoading = useIOSelector(isLoadingFeaturedInstitutionsSelector);
+
+ useOnFirstRender(() => dispatch(featuredInstitutionsGet.request()));
+
+ const handlePress = useCallback(
+ ({ fiscal_code, name }: Institution) => {
+ navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, {
+ screen: SERVICES_ROUTES.INSTITUTION_SERVICES,
+ params: {
+ institutionId: fiscal_code,
+ institutionName: name
+ }
+ });
+ },
+ [navigation]
+ );
+
+ const mappedFeaturedInstitutions = useMemo(
+ () =>
+ featuredInstitutions.map(props => ({
+ ...props,
+ onPress: () => handlePress(props)
+ })),
+ [featuredInstitutions, handlePress]
+ );
+
+ const isVisible = useMemo(
+ () => isLoading || mappedFeaturedInstitutions.length > 0,
+ [isLoading, mappedFeaturedInstitutions]
+ );
+
+ if (isError || !isVisible) {
+ return null;
+ }
+
+ return (
+ <>
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/ts/features/services/home/components/FeaturedServiceCard.tsx b/ts/features/services/home/components/FeaturedServiceCard.tsx
index 0aab69075dd..ae8fc0dc18b 100644
--- a/ts/features/services/home/components/FeaturedServiceCard.tsx
+++ b/ts/features/services/home/components/FeaturedServiceCard.tsx
@@ -1,3 +1,6 @@
+import React from "react";
+import { StyleSheet, View } from "react-native";
+import Placeholder from "rn-placeholder";
import {
Avatar,
Badge,
@@ -5,13 +8,12 @@ import {
IOColors,
IOSpacingScale,
IOVisualCostants,
+ TestID,
VSpacer
} from "@pagopa/io-app-design-system";
-import React from "react";
-import { StyleSheet, View } from "react-native";
-import Placeholder from "rn-placeholder";
import I18n from "../../../../i18n";
import { WithTestID } from "../../../../types/WithTestID";
+import { CardPressableBase } from "../../common/components/CardPressableBase";
import { logoForService } from "../utils";
import OrganizationNameLabel from "./OrganizationNameLabel";
@@ -61,35 +63,43 @@ const styles = StyleSheet.create({
});
const FeaturedServiceCard = (props: FeaturedServiceCardProps) => (
-
-
-
- {props.isNew && }
-
-
-
- {props.name}
-
- {props.organizationName && (
- <>
-
-
- {props.organizationName}
-
- >
- )}
+
+
+
+ {props.isNew && (
+
+ )}
+
+
+
+ {props.name}
+
+ {props.organizationName && (
+ <>
+
+
+ {props.organizationName}
+
+ >
+ )}
+
-
+
);
-const FeaturedServiceCardSkeleton = ({ testID }: WithTestID) => (
+const FeaturedServiceCardSkeleton = ({ testID }: TestID) => (
{
+ const dispatch = useIODispatch();
+ const navigation = useIONavigation();
+
+ const featuredServices = useIOSelector(featuredServicesSelector);
+ const isError = useIOSelector(isErrorFeaturedServicesSelector);
+ const isLoading = useIOSelector(isLoadingFeaturedServicesSelector);
+
+ useOnFirstRender(() => dispatch(featuredServicesGet.request()));
+
+ const handlePress = useCallback(
+ (serviceId: string) => {
+ navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, {
+ screen: SERVICES_ROUTES.SERVICE_DETAIL,
+ params: {
+ serviceId: serviceId as NonEmptyString
+ }
+ });
+ },
+ [navigation]
+ );
+
+ const mappedFeaturedServices = useMemo(
+ () =>
+ featuredServices.map(({ organization_name, ...rest }) => ({
+ ...rest,
+ organizationName: organization_name,
+ onPress: () => handlePress(rest.id)
+ })),
+ [featuredServices, handlePress]
+ );
+
+ const isVisible = useMemo(
+ () => isLoading || mappedFeaturedServices.length > 0,
+ [isLoading, mappedFeaturedServices]
+ );
+
+ if (isError || !isVisible) {
+ return null;
+ }
+
+ return (
+ <>
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/ts/features/services/home/components/ServicesHomeIntitutionList.tsx b/ts/features/services/home/components/ServicesHomeIntitutionList.tsx
deleted file mode 100644
index 1717a758a65..00000000000
--- a/ts/features/services/home/components/ServicesHomeIntitutionList.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import React, { useCallback, useEffect } from "react";
-import { FlatList, ListRenderItemInfo } from "react-native";
-import {
- Divider,
- IOStyles,
- IOToast,
- ListItemHeader,
- ListItemNav,
- VSpacer
-} from "@pagopa/io-app-design-system";
-import { Institution } from "../../../../../definitions/services/Institution";
-import I18n from "../../../../i18n";
-import { useIONavigation } from "../../../../navigation/params/AppParamsList";
-import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender";
-import { useFirstRender } from "../../common/hooks/useFirstRender";
-import { SERVICES_ROUTES } from "../../common/navigation/routes";
-import { useInstitutionsFetcher } from "../hooks/useInstitutionsFetcher";
-import { logoForInstitution } from "../utils";
-import { InstitutionListItemSkeleton } from "./InstitutionListItemSkeleton";
-
-export const ServicesHomeIntitutionList = () => {
- const isFirstRender = useFirstRender();
- const navigation = useIONavigation();
-
- const {
- currentPage,
- data,
- fetchInstitutions,
- isError,
- isLoading,
- isUpdating,
- refreshInstitutions
- } = useInstitutionsFetcher();
-
- useOnFirstRender(() => fetchInstitutions(0));
-
- useEffect(() => {
- if (!isFirstRender && isError) {
- IOToast.error(I18n.t("global.genericError"));
- }
- }, [isFirstRender, isError]);
-
- const handleEndReached = useCallback(
- () => fetchInstitutions(currentPage + 1),
- [currentPage, fetchInstitutions]
- );
-
- const navigateToInstitution = useCallback(
- (institution: Institution) =>
- navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, {
- screen: SERVICES_ROUTES.INSTITUTION_SERVICES,
- params: {
- institutionId: institution.id,
- institutionName: institution.name
- }
- }),
- [navigation]
- );
-
- const renderItem = useCallback(
- ({ item }: ListRenderItemInfo) => (
- navigateToInstitution(item)}
- accessibilityLabel={item.name}
- avatarProps={{
- logoUri: logoForInstitution(item)
- }}
- />
- ),
- [navigateToInstitution]
- );
-
- const renderListFooterComponent = useCallback(() => {
- if (isUpdating && currentPage > 0) {
- return (
- <>
-
-
-
-
-
- >
- );
- }
- return ;
- }, [currentPage, isUpdating]);
-
- const ListHeaderComponent = (
-
- );
-
- if (isFirstRender || isLoading) {
- return (
- }
- contentContainerStyle={IOStyles.horizontalContentPadding}
- data={Array.from({ length: 5 })}
- keyExtractor={(_, index) => `placeholder-${index}`}
- renderItem={() => }
- onRefresh={refreshInstitutions}
- refreshing={isUpdating}
- />
- );
- }
-
- return (
- }
- contentContainerStyle={IOStyles.horizontalContentPadding}
- data={data?.institutions || []}
- keyExtractor={(item, index) => `institution-${item.id}-${index}`}
- renderItem={renderItem}
- onEndReached={handleEndReached}
- onEndReachedThreshold={0.001}
- onRefresh={refreshInstitutions}
- refreshing={isUpdating}
- ListFooterComponent={renderListFooterComponent}
- />
- );
-};
diff --git a/ts/features/services/home/components/__tests__/FeaturedInstitutionCard.test.tsx b/ts/features/services/home/components/__tests__/FeaturedInstitutionCard.test.tsx
new file mode 100644
index 00000000000..a8b25fe8012
--- /dev/null
+++ b/ts/features/services/home/components/__tests__/FeaturedInstitutionCard.test.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { createStore } from "redux";
+import { applicationChangeState } from "../../../../../store/actions/application";
+import { appReducer } from "../../../../../store/reducers";
+import { GlobalState } from "../../../../../store/reducers/types";
+import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper";
+import { SERVICES_ROUTES } from "../../../common/navigation/routes";
+import {
+ FeaturedInstitutionCard,
+ FeaturedInstitutionCardProps
+} from "../FeaturedInstitutionCard";
+
+const testID = "FeaturedInstitutionCardTestID";
+
+describe("FeaturedInstitutionCard", () => {
+ it(`should match the snapshot`, () => {
+ const component = render({
+ id: "1",
+ name: "### Institution ###",
+ accessibilityLabel: "### Accessibility Label ###",
+ onPress: () => undefined,
+ testID
+ });
+ expect(component).toMatchSnapshot();
+ });
+
+ it(`should render card without pressable wrapper`, () => {
+ const { queryByTestId } = render({
+ id: "1",
+ name: "### Institution ###",
+ accessibilityLabel: "### Accessibility Label ###",
+ testID
+ });
+ expect(queryByTestId(`${testID}-pressable`)).toBeNull();
+ });
+
+ it(`should match the snapshot when isNew is true`, () => {
+ const component = render({
+ id: "1",
+ name: "### Institution ###",
+ accessibilityLabel: "### Accessibility Label ###",
+ onPress: () => undefined,
+ isNew: true,
+ testID
+ });
+ expect(component).toMatchSnapshot();
+ });
+});
+
+function render(props: FeaturedInstitutionCardProps) {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+ return renderScreenWithNavigationStoreContext(
+ () => ,
+ SERVICES_ROUTES.SERVICES_HOME,
+ {},
+ createStore(appReducer, globalState as any)
+ );
+}
diff --git a/ts/features/services/home/components/__tests__/FeaturedServiceCard.test.tsx b/ts/features/services/home/components/__tests__/FeaturedServiceCard.test.tsx
new file mode 100644
index 00000000000..800bb8b080f
--- /dev/null
+++ b/ts/features/services/home/components/__tests__/FeaturedServiceCard.test.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { createStore } from "redux";
+import { applicationChangeState } from "../../../../../store/actions/application";
+import { appReducer } from "../../../../../store/reducers";
+import { GlobalState } from "../../../../../store/reducers/types";
+import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper";
+import { SERVICES_ROUTES } from "../../../common/navigation/routes";
+import {
+ FeaturedServiceCard,
+ FeaturedServiceCardProps
+} from "../FeaturedServiceCard";
+
+const testID = "FeaturedServiceCardTestID";
+
+describe("FeaturedServiceCard", () => {
+ it(`should match the snapshot`, () => {
+ const component = render({
+ id: "1",
+ name: "### Service ###",
+ accessibilityLabel: "### Accessibility Label ###",
+ onPress: () => undefined,
+ testID
+ });
+ expect(component).toMatchSnapshot();
+ });
+
+ it(`should render card without pressable wrapper`, () => {
+ const { queryByTestId } = render({
+ id: "1",
+ name: "### Service ###",
+ accessibilityLabel: "### Accessibility Label ###",
+ testID
+ });
+ expect(queryByTestId(`${testID}-pressable`)).toBeNull();
+ });
+
+ it(`should match the snapshot when isNew is true`, () => {
+ const component = render({
+ id: "1",
+ name: "### Service ###",
+ accessibilityLabel: "### Accessibility Label ###",
+ onPress: () => undefined,
+ isNew: true,
+ testID
+ });
+ expect(component).toMatchSnapshot();
+ });
+});
+
+function render(props: FeaturedServiceCardProps) {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+ return renderScreenWithNavigationStoreContext(
+ () => ,
+ SERVICES_ROUTES.SERVICES_HOME,
+ {},
+ createStore(appReducer, globalState as any)
+ );
+}
diff --git a/ts/features/services/home/components/__tests__/__snapshots__/FeaturedInstitutionCard.test.tsx.snap b/ts/features/services/home/components/__tests__/__snapshots__/FeaturedInstitutionCard.test.tsx.snap
new file mode 100644
index 00000000000..e38333bde1b
--- /dev/null
+++ b/ts/features/services/home/components/__tests__/__snapshots__/FeaturedInstitutionCard.test.tsx.snap
@@ -0,0 +1,990 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FeaturedInstitutionCard should match the snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SERVICES_HOME
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ### Institution ###
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`FeaturedInstitutionCard should match the snapshot when isNew is true 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SERVICES_HOME
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ### Institution ###
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ts/features/services/home/components/__tests__/__snapshots__/FeaturedServiceCard.test.tsx.snap b/ts/features/services/home/components/__tests__/__snapshots__/FeaturedServiceCard.test.tsx.snap
new file mode 100644
index 00000000000..f05754803c8
--- /dev/null
+++ b/ts/features/services/home/components/__tests__/__snapshots__/FeaturedServiceCard.test.tsx.snap
@@ -0,0 +1,1024 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FeaturedServiceCard should match the snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SERVICES_HOME
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ### Service ###
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`FeaturedServiceCard should match the snapshot when isNew is true 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SERVICES_HOME
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ New
+
+
+
+
+
+ ### Service ###
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ts/features/services/home/hooks/useInstitutionsFetcher.tsx b/ts/features/services/home/hooks/useInstitutionsFetcher.tsx
index b717d358fc1..19bbe29f640 100644
--- a/ts/features/services/home/hooks/useInstitutionsFetcher.tsx
+++ b/ts/features/services/home/hooks/useInstitutionsFetcher.tsx
@@ -1,3 +1,4 @@
+import { useCallback, useEffect, useState } from "react";
import { ScopeTypeEnum } from "../../../../../definitions/services/ScopeType";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { paginatedInstitutionsGet } from "../store/actions";
@@ -22,27 +23,44 @@ export const useInstitutionsFetcher = () => {
const isUpdating = useIOSelector(isUpdatingPaginatedInstitutionsSelector);
const isError = useIOSelector(isErrorPaginatedInstitutionsSelector);
- const fetchPage = (page: number) => {
- if (!isLoading && !isUpdating) {
- dispatch(
- paginatedInstitutionsGet.request({
- offset: page * LIMIT,
- limit: LIMIT,
- scope: ScopeTypeEnum.NATIONAL
- })
- );
- }
- };
+ const [isRefreshing, setIsRefreshing] = useState(false);
- const fetchInstitutions = (page: number) => {
- if (isLastPage) {
- return;
+ useEffect(() => {
+ if (isRefreshing && !isUpdating) {
+ setIsRefreshing(false);
}
+ }, [isRefreshing, isUpdating]);
- fetchPage(page);
- };
+ const fetchPage = useCallback(
+ (page: number) => {
+ if (!isLoading && !isUpdating) {
+ dispatch(
+ paginatedInstitutionsGet.request({
+ offset: page * LIMIT,
+ limit: LIMIT,
+ scope: ScopeTypeEnum.NATIONAL
+ })
+ );
+ }
+ },
+ [dispatch, isLoading, isUpdating]
+ );
+
+ const fetchInstitutions = useCallback(
+ (page: number) => {
+ if (isLastPage) {
+ return;
+ }
+
+ fetchPage(page);
+ },
+ [isLastPage, fetchPage]
+ );
- const refreshInstitutions = () => fetchPage(0);
+ const refreshInstitutions = useCallback(() => {
+ setIsRefreshing(true);
+ fetchPage(0);
+ }, [fetchPage]);
return {
currentPage,
@@ -50,6 +68,7 @@ export const useInstitutionsFetcher = () => {
isError,
isLoading,
isUpdating,
+ isRefreshing,
fetchInstitutions,
refreshInstitutions
};
diff --git a/ts/features/services/home/screens/ServicesHomeScreen.tsx b/ts/features/services/home/screens/ServicesHomeScreen.tsx
index e06678ab6a4..152b55ecf19 100644
--- a/ts/features/services/home/screens/ServicesHomeScreen.tsx
+++ b/ts/features/services/home/screens/ServicesHomeScreen.tsx
@@ -1,4 +1,141 @@
-import React from "react";
-import { ServicesHomeIntitutionList } from "../components/ServicesHomeIntitutionList";
+import React, { useCallback, useEffect } from "react";
+import { FlatList, ListRenderItemInfo, StyleSheet } from "react-native";
+import {
+ Divider,
+ IOStyles,
+ IOToast,
+ ListItemHeader,
+ ListItemNav,
+ VSpacer
+} from "@pagopa/io-app-design-system";
+import I18n from "../../../../i18n";
+import { Institution } from "../../../../../definitions/services/Institution";
+import { useIONavigation } from "../../../../navigation/params/AppParamsList";
+import { useIODispatch } from "../../../../store/hooks";
+import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender";
+import { InstitutionListSkeleton } from "../../common/components/InstitutionListSkeleton";
+import { useFirstRender } from "../../common/hooks/useFirstRender";
+import { SERVICES_ROUTES } from "../../common/navigation/routes";
+import { useInstitutionsFetcher } from "../hooks/useInstitutionsFetcher";
+import { featuredInstitutionsGet, featuredServicesGet } from "../store/actions";
+import { logoForInstitution } from "../utils";
+import { FeaturedInstitutionList } from "../components/FeaturedInstitutionList";
+import { FeaturedServiceList } from "../components/FeaturedServiceList";
-export const ServicesHomeScreen = () => ;
+const styles = StyleSheet.create({
+ scrollContentContainer: {
+ flexGrow: 1
+ }
+});
+
+export const ServicesHomeScreen = () => {
+ const dispatch = useIODispatch();
+ const navigation = useIONavigation();
+ const isFirstRender = useFirstRender();
+
+ const {
+ currentPage,
+ data,
+ fetchInstitutions,
+ isError,
+ isLoading,
+ isUpdating,
+ isRefreshing,
+ refreshInstitutions
+ } = useInstitutionsFetcher();
+
+ useOnFirstRender(() => fetchInstitutions(0));
+
+ useEffect(() => {
+ if (!isFirstRender && isError) {
+ IOToast.error(I18n.t("global.genericError"));
+ }
+ }, [isFirstRender, isError]);
+
+ const renderListEmptyComponent = useCallback(() => {
+ if (isFirstRender || isLoading) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+ return <>>;
+ }, [isFirstRender, isLoading]);
+
+ const renderListHeaderComponent = useCallback(
+ () => (
+ <>
+
+
+
+ >
+ ),
+ []
+ );
+
+ const renderListFooterComponent = useCallback(() => {
+ if (isUpdating && !isRefreshing) {
+ return ;
+ }
+ return ;
+ }, [isUpdating, isRefreshing]);
+
+ const handleRefresh = useCallback(() => {
+ dispatch(featuredServicesGet.request());
+ dispatch(featuredInstitutionsGet.request());
+ refreshInstitutions();
+ }, [dispatch, refreshInstitutions]);
+
+ const handleEndReached = useCallback(
+ () => fetchInstitutions(currentPage + 1),
+ [currentPage, fetchInstitutions]
+ );
+
+ const navigateToInstitution = useCallback(
+ (institution: Institution) =>
+ navigation.navigate(SERVICES_ROUTES.SERVICES_NAVIGATOR, {
+ screen: SERVICES_ROUTES.INSTITUTION_SERVICES,
+ params: {
+ institutionId: institution.id,
+ institutionName: institution.name
+ }
+ }),
+ [navigation]
+ );
+
+ const renderInstitutionItem = useCallback(
+ ({ item }: ListRenderItemInfo) => (
+ navigateToInstitution(item)}
+ accessibilityLabel={item.name}
+ avatarProps={{
+ logoUri: logoForInstitution(item)
+ }}
+ />
+ ),
+ [navigateToInstitution]
+ );
+
+ return (
+ }
+ ListEmptyComponent={renderListEmptyComponent}
+ ListFooterComponent={renderListFooterComponent}
+ ListHeaderComponent={renderListHeaderComponent}
+ contentContainerStyle={[
+ styles.scrollContentContainer,
+ IOStyles.horizontalContentPadding
+ ]}
+ data={data?.institutions || []}
+ keyExtractor={(item, index) => `institution-${item.id}-${index}`}
+ onEndReached={handleEndReached}
+ onEndReachedThreshold={0.001}
+ onRefresh={handleRefresh}
+ refreshing={isRefreshing}
+ renderItem={renderInstitutionItem}
+ />
+ );
+};
diff --git a/ts/features/services/home/store/reducers/__tests__/store.test.ts b/ts/features/services/home/store/reducers/__tests__/store.test.ts
index e760008aaf6..2bcad4c329f 100644
--- a/ts/features/services/home/store/reducers/__tests__/store.test.ts
+++ b/ts/features/services/home/store/reducers/__tests__/store.test.ts
@@ -9,8 +9,6 @@ import {
isLoadingFeaturedInstitutionsSelector,
isLoadingFeaturedServicesSelector,
isLoadingPaginatedInstitutionsSelector,
- isUpdatingFeaturedInstitutionsSelector,
- isUpdatingFeaturedServicesSelector,
isUpdatingPaginatedInstitutionsSelector,
paginatedInstitutionsCurrentPageSelector,
paginatedInstitutionsLastPageSelector,
@@ -492,21 +490,19 @@ describe("Services home featuredInstitutions selectors", () => {
})
)
);
- expect(featuredInstitutions).toStrictEqual({
- institutions: MOCK_INSTITUTIONS
- });
+ expect(featuredInstitutions).toStrictEqual(MOCK_INSTITUTIONS);
});
it("should return undefined when not pot.some", () => {
expect(
featuredInstitutionsSelector(appReducer(undefined, {} as Action))
- ).toBeUndefined();
+ ).toStrictEqual([]);
expect(
featuredInstitutionsSelector(
appReducer({} as GlobalState, featuredInstitutionsGet.request())
)
- ).toBeUndefined();
+ ).toStrictEqual([]);
expect(
featuredInstitutionsSelector(
@@ -515,7 +511,7 @@ describe("Services home featuredInstitutions selectors", () => {
featuredInstitutionsGet.failure(MOCK_NETWORK_ERROR)
)
)
- ).toBeUndefined();
+ ).toStrictEqual([]);
});
});
@@ -547,51 +543,6 @@ describe("Services home featuredInstitutions selectors", () => {
});
});
- describe("isUpdatingFeaturedInstitutionsSelector", () => {
- it("should return true when pot.updating", () => {
- const state = appReducer(undefined, applicationChangeState("active"));
- const store = createStore(appReducer, state as any);
-
- store.dispatch(
- featuredInstitutionsGet.success({
- institutions: MOCK_INSTITUTIONS
- })
- );
- store.dispatch(featuredInstitutionsGet.request());
-
- const isUpdating = isUpdatingFeaturedInstitutionsSelector(
- store.getState()
- );
-
- expect(isUpdating).toStrictEqual(true);
- });
-
- it("should return false when not pot.updating", () => {
- expect(
- isUpdatingFeaturedInstitutionsSelector(
- appReducer(undefined, {} as Action)
- )
- ).toStrictEqual(false);
-
- expect(
- isUpdatingFeaturedInstitutionsSelector(
- appReducer({} as GlobalState, featuredInstitutionsGet.request())
- )
- ).toStrictEqual(false);
-
- expect(
- isUpdatingFeaturedInstitutionsSelector(
- appReducer(
- {} as GlobalState,
- featuredInstitutionsGet.success({
- institutions: MOCK_INSTITUTIONS
- })
- )
- )
- ).toStrictEqual(false);
- });
- });
-
describe("isErrorFeaturedInstitutionsSelector", () => {
it("should return true when pot.error", () => {
const isError = isErrorFeaturedInstitutionsSelector(
@@ -678,21 +629,19 @@ describe("Services home featuredServices selectors", () => {
})
)
);
- expect(featuredServices).toStrictEqual({
- services: MOCK_FEATURED_SERVICES
- });
+ expect(featuredServices).toStrictEqual(MOCK_FEATURED_SERVICES);
});
it("should return undefined when not pot.some", () => {
expect(
featuredServicesSelector(appReducer(undefined, {} as Action))
- ).toBeUndefined();
+ ).toStrictEqual([]);
expect(
featuredServicesSelector(
appReducer({} as GlobalState, featuredServicesGet.request())
)
- ).toBeUndefined();
+ ).toStrictEqual([]);
expect(
featuredServicesSelector(
@@ -701,7 +650,7 @@ describe("Services home featuredServices selectors", () => {
featuredServicesGet.failure(MOCK_NETWORK_ERROR)
)
)
- ).toBeUndefined();
+ ).toStrictEqual([]);
});
});
@@ -731,47 +680,6 @@ describe("Services home featuredServices selectors", () => {
});
});
- describe("isUpdatingFeaturedServicesSelector", () => {
- it("should return true when pot.updating", () => {
- const state = appReducer(undefined, applicationChangeState("active"));
- const store = createStore(appReducer, state as any);
-
- store.dispatch(
- featuredServicesGet.success({
- services: MOCK_FEATURED_SERVICES
- })
- );
- store.dispatch(featuredServicesGet.request());
-
- const isUpdating = isUpdatingFeaturedServicesSelector(store.getState());
-
- expect(isUpdating).toStrictEqual(true);
- });
-
- it("should return false when not pot.updating", () => {
- expect(
- isUpdatingFeaturedServicesSelector(appReducer(undefined, {} as Action))
- ).toStrictEqual(false);
-
- expect(
- isUpdatingFeaturedServicesSelector(
- appReducer({} as GlobalState, featuredServicesGet.request())
- )
- ).toStrictEqual(false);
-
- expect(
- isUpdatingFeaturedServicesSelector(
- appReducer(
- {} as GlobalState,
- featuredServicesGet.success({
- services: MOCK_FEATURED_SERVICES
- })
- )
- )
- ).toStrictEqual(false);
- });
- });
-
describe("isErrorFeaturedServicesSelector", () => {
it("should return true when pot.error", () => {
const isError = isErrorFeaturedServicesSelector(
diff --git a/ts/features/services/home/store/reducers/index.ts b/ts/features/services/home/store/reducers/index.ts
index faa7cbd604a..b406de31f9b 100644
--- a/ts/features/services/home/store/reducers/index.ts
+++ b/ts/features/services/home/store/reducers/index.ts
@@ -32,7 +32,7 @@ const homeReducer = (
action: Action
): ServicesHomeState => {
switch (action.type) {
- // Fetch Institutions actions
+ // Get Institutions actions
case getType(paginatedInstitutionsGet.request):
if (pot.isNone(state.paginatedInstitutions)) {
return {
@@ -159,7 +159,14 @@ export const featuredInstitutionsPotSelector = createSelector(
export const featuredInstitutionsSelector = createSelector(
featuredInstitutionsPotSelector,
- pot.toUndefined
+ featuredInstitutionsPot =>
+ pot.getOrElse(
+ pot.map(
+ featuredInstitutionsPot,
+ featuredInstitutions => featuredInstitutions.institutions
+ ),
+ []
+ )
);
export const featuredServicesPotSelector = createSelector(
@@ -169,7 +176,14 @@ export const featuredServicesPotSelector = createSelector(
export const featuredServicesSelector = createSelector(
featuredServicesPotSelector,
- pot.toUndefined
+ featuredServicesPot =>
+ pot.getOrElse(
+ pot.map(
+ featuredServicesPot,
+ featuredServices => featuredServices.services
+ ),
+ []
+ )
);
export const isLoadingPaginatedInstitutionsSelector = (state: GlobalState) =>
@@ -184,18 +198,12 @@ export const isErrorPaginatedInstitutionsSelector = (state: GlobalState) =>
export const isLoadingFeaturedInstitutionsSelector = (state: GlobalState) =>
pipe(state, featuredInstitutionsPotSelector, pot.isLoading);
-export const isUpdatingFeaturedInstitutionsSelector = (state: GlobalState) =>
- pipe(state, featuredInstitutionsPotSelector, pot.isUpdating);
-
export const isErrorFeaturedInstitutionsSelector = (state: GlobalState) =>
pipe(state, featuredInstitutionsPotSelector, pot.isError);
export const isLoadingFeaturedServicesSelector = (state: GlobalState) =>
pipe(state, featuredServicesPotSelector, pot.isLoading);
-export const isUpdatingFeaturedServicesSelector = (state: GlobalState) =>
- pipe(state, featuredServicesPotSelector, pot.isUpdating);
-
export const isErrorFeaturedServicesSelector = (state: GlobalState) =>
pipe(state, featuredServicesPotSelector, pot.isError);
From 45d20bc77ed78589d826fd806096dab06bb6995b Mon Sep 17 00:00:00 2001
From: Andrea
Date: Thu, 23 May 2024 17:25:28 +0200
Subject: [PATCH 5/5] feat: [IOCOM-1133,IOCOM-1235,IOCOM-1372] Push
Notification Opt In screen, new DS (#5734)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
⚠️ This PR depends on #5732 ⚠️
⚠️ This PR depends on
[io-app-design-system#253](https://github.com/pagopa/io-app-design-system/pull/253)
⚠️
⚠️ This PR depends on
[io-app-design-system#256](https://github.com/pagopa/io-app-design-system/pull/256)
⚠️
## Short description
This PR aligns the Push Notifications Opt In screen and the profile push
notification settings to the new DS
|Both on
upper part|Both on
scrolled down|Bottom sheet|
|-|-|-|
|![On1](https://github.com/pagopa/io-app/assets/5150343/66a756b4-9706-4cda-a20a-b5ebf92e6f1d)|![Simulator
Screenshot - iPhone 15 - 2024-05-10 at 12 26
26](https://github.com/pagopa/io-app/assets/5150343/6e6c0ed5-c44c-4e81-ae9c-477ff6e22658)|![OnBS](https://github.com/pagopa/io-app/assets/5150343/9b079428-5262-4f79-b386-bc7e0b6af892)|
|Preview Off
Reminder On|Preview On
Reminder Off|Both Off|
|-|-|-|
|![Pre](https://github.com/pagopa/io-app/assets/5150343/9955c886-ecf5-4401-8e86-7c11a6c7c486)|![Rem](https://github.com/pagopa/io-app/assets/5150343/ea87c7b0-3ca9-435d-993d-2a2994ff9741)|![Off](https://github.com/pagopa/io-app/assets/5150343/0de87ad0-824a-4eca-8251-67ed8ba58ef1)|
|Profile
Both on|Profile
Bottom sheet|
|-|-|
|![ProfileOnPreOnRem](https://github.com/pagopa/io-app/assets/5150343/4bd1d77a-985f-4d8b-89b0-771cedf5ae8c)|![ProfileBS](https://github.com/pagopa/io-app/assets/5150343/a4ab71eb-e5b1-493a-9e4c-d487d1c0e0ec)|
|Profile
On Off|Profile
Off On|Profile
Off Off|
|-|-|-|
|![ProfileOnPreOffRem](https://github.com/pagopa/io-app/assets/5150343/f8fb2554-e94c-46c9-90da-198195d24f28)|![ProfileOffPreOnRem](https://github.com/pagopa/io-app/assets/5150343/4dc52f4a-8165-4495-90cd-771d1009ea63)|![ProfileOffPreOffRem](https://github.com/pagopa/io-app/assets/5150343/494f7064-d6e7-46a2-92c8-5782c161beda)|
## List of changes proposed in this pull request
- All related onboarding screens have been ported to the new DS
- A lot of test snapshot have been updated due to a change in margins on
the DS library
## How to test
Using the io-dev-api-server, configure the profile in order to have both
`reminder_status` and `push_notifications_content_type` set to
undefined. Perform a login and the opt-in screen should appear. Check
that both values are properly set after tapping the "Continue" button.
For the profile screen, navigate to it and change switches' values
---
.env.local | 2 -
.env.production | 2 -
img/onboarding/notification_blue.png | Bin 2746 -> 0 bytes
img/onboarding/notification_blue@2x.png | Bin 6256 -> 0 bytes
img/onboarding/notification_blue@3x.png | Bin 9371 -> 0 bytes
img/onboarding/notification_white.png | Bin 2709 -> 2474 bytes
img/onboarding/notification_white@2x.png | Bin 6027 -> 5602 bytes
img/onboarding/notification_white@3x.png | Bin 9046 -> 8769 bytes
locales/it/index.yml | 16 +-
.../ui/RNavScreenWithLargeHeader.tsx | 2 +-
ts/config.ts | 3 -
.../components/NotificationPreviewSample.tsx | 25 +-
.../NotificationsPreferencesPreview.tsx | 7 +-
.../ProfileNotificationsSettings.tsx | 86 +
.../NotificationPreviewSample.test.tsx | 161 +-
.../NotificationsPreferencesPreview.test.tsx | 48 +
.../ProfileNotificationsSettings.test.tsx | 77 +
.../NotificationPreviewSample.test.tsx.snap | 2157 +
...ificationsPreferencesPreview.test.tsx.snap | 2293 +
...ProfileNotificationsSettings.test.tsx.snap | 169793 +++++++++++++++
.../hooks/usePreviewMoreInfo.tsx | 30 +-
...checkNotificationsPermissionsSaga.test.tsx | 82 -
...checkNotificationsPreferencesSaga.test.tsx | 205 +
.../checkNotificationsPermissionsSaga.ts | 43 -
.../checkNotificationsPreferencesSaga.ts | 60 +-
...boardingNotificationsInfoScreenConsent.tsx | 317 +-
...boardingNotificationsPreferencesScreen.tsx | 270 +-
...ingNotificationsInfoScreenConsent.test.tsx | 56 +-
...ingNotificationsPreferencesScreen.test.tsx | 185 +-
...tificationsInfoScreenConsent.test.tsx.snap | 1489 +
...tificationsPreferencesScreen.test.tsx.snap | 8476 +
.../utils/configurePushNotification.ts | 8 +-
ts/navigation/OnboardingNavigator.tsx | 18 +-
ts/navigation/ProfileNavigator.tsx | 11 +-
...checkNotificationsPermissionsSaga.test.tsx | 82 -
.../NotificationsPreferencesScreen.tsx | 184 +-
ts/screens/profile/PreferencesScreen.tsx | 2 -
.../NotificationsPreferencesScreen.test.tsx | 295 +-
...tificationsPreferencesScreen.test.tsx.snap | 24715 +++
ts/store/reducers/__tests__/profile.test.ts | 442 +-
ts/store/reducers/profile.ts | 39 +-
41 files changed, 210600 insertions(+), 1081 deletions(-)
delete mode 100644 img/onboarding/notification_blue.png
delete mode 100644 img/onboarding/notification_blue@2x.png
delete mode 100644 img/onboarding/notification_blue@3x.png
create mode 100644 ts/features/pushNotifications/components/ProfileNotificationsSettings.tsx
create mode 100644 ts/features/pushNotifications/components/__tests__/NotificationsPreferencesPreview.test.tsx
create mode 100644 ts/features/pushNotifications/components/__tests__/ProfileNotificationsSettings.test.tsx
create mode 100644 ts/features/pushNotifications/components/__tests__/__snapshots__/NotificationPreviewSample.test.tsx.snap
create mode 100644 ts/features/pushNotifications/components/__tests__/__snapshots__/NotificationsPreferencesPreview.test.tsx.snap
create mode 100644 ts/features/pushNotifications/components/__tests__/__snapshots__/ProfileNotificationsSettings.test.tsx.snap
delete mode 100644 ts/features/pushNotifications/sagas/__tests__/checkNotificationsPermissionsSaga.test.tsx
create mode 100644 ts/features/pushNotifications/sagas/__tests__/checkNotificationsPreferencesSaga.test.tsx
delete mode 100644 ts/features/pushNotifications/sagas/checkNotificationsPermissionsSaga.ts
create mode 100644 ts/features/pushNotifications/screens/__tests__/__snapshots__/OnboardingNotificationsInfoScreenConsent.test.tsx.snap
create mode 100644 ts/features/pushNotifications/screens/__tests__/__snapshots__/OnboardingNotificationsPreferencesScreen.test.tsx.snap
delete mode 100644 ts/sagas/startup/__tests__/checkNotificationsPermissionsSaga.test.tsx
create mode 100644 ts/screens/profile/__test__/__snapshots__/NotificationsPreferencesScreen.test.tsx.snap
diff --git a/.env.local b/.env.local
index 14dab675003..b748bf88d3b 100644
--- a/.env.local
+++ b/.env.local
@@ -59,8 +59,6 @@ PREMIUM_MESSAGES_OPT_IN_ENABLED=YES
CDC_ENABLED = YES
# Scan additional barcodes (E.g. Data Matrix) in the payment section
SCAN_ADDITIONAL_BARCODES_ENABLED = YES
-# Opt-in for reminder push notifications
-REMINDERS_OPT_IN_ENABLED=YES
# FCI (Firma con IO) feature
FCI_ENABLED=YES
# IDPay
diff --git a/.env.production b/.env.production
index a17b711a25b..98049503f0d 100644
--- a/.env.production
+++ b/.env.production
@@ -59,8 +59,6 @@ PREMIUM_MESSAGES_OPT_IN_ENABLED=YES
CDC_ENABLED = YES
# Scan additional barcodes (E.g. Data Matrix) in the payment section
SCAN_ADDITIONAL_BARCODES_ENABLED = YES
-# Opt-in for reminder push notifications
-REMINDERS_OPT_IN_ENABLED=YES
# FCI (Firma con IO) feature
FCI_ENABLED=YES
# IDPay
diff --git a/img/onboarding/notification_blue.png b/img/onboarding/notification_blue.png
deleted file mode 100644
index 378ce65d796a741c7fab02ea32d564065a2e0e1f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 2746
zcmbVOc{mj69-giuNt-Rk&PbCjBl|ki5N1SqL+rVM`C&W$w004dq^Q*QT
z&E$}LoSU;gV9EX92yeK#V*~)eC-5&G0TdLS=Nul1u!TVZ_+jy7&gQ7UiM0s;(45J~
z_B;jvoIqGyHMxm7vPQ8E7F?1T*cd?V%Zm6fCe=k{{ZW;xEpPJVq&Qec+92H02Ob_^
zIVfYR`k;KSPB>z@($;&_H0)S~sgU2#cR;;x
z>5xQ<*^HWu?_sQN$?;y-G#SS)oGDSn0|tSETm?q(G+GtBJx9fYwu_GX`h|Xq(`@&WtZ=X%;!Xyv8oE8wB@Y^>M8|fFiAymqp3Zsr
zt(gC>L$SN$15=XDq@Ge7V|enDiA+<j?{+HQXrN(*tC_nL{oX+2C}z7gQ0IGm
zIeQ~(eJb+(St^%$eb$$3b_e-!9ai;;=k7>;-|fjuIGvK9x3J!Ep!7rj
z4YqmY%UiFoNa>D6PI`{9mBiij2>Ti@KRl+dOZ6FnM>&(!Lel6pkr?B<)a#m;C#@fP
zMxF@1A5=jv*y+};Kt}WS$<+hM!7&qnm$L{Fpoo^?slIHb*fTY1dxun|Ss-hwH`6&%
zC6Ok6JJJTm*a=b#U}TxJw&~*!V~pz)x%G#InJ5d)EKL;|5|z9=LxyKDd&Z33`o%nT
z18T&N9I&!x3yJwtPmj&@80D+UajhwQqPyOZPGeoD5?M2`q15I9mv-0aQ3ZAmB~y>h
z_Kh?gvFL?cB|Ef$Wmy)m78J4J7t#193BSJ;Wr=YivaAgyAq^YZv9q?z*xkJGU;WHqk(3>cWAT#2bov
z6}_{PIzj<0AP>iSqwL96sAorExjF3aV4nBhTEhyI#j%4*XolFKnU
zraBN8ck{4&e(*MKvlE9;suOPWKu$D!HXaK?os&fKQx}6-Xy@mR(Rvk=OxY#>st^ZR
zk#0~75cqEB_oI8+5}_|m#w)06GR!@MVcorg0i1_b^pTtmJ4nHZp!4eMZ7VJ)w8tZ9
z&mD8*yu>%fd+>kAdRGRumPf$4i8kfG{dPU?%Cl7^qjYyq6}kcX%LJxPzj>J;%>H0b
zv?Xd0-Rd=_dI&6eX?5Z05+zF6xvE7%EOrvCZz`r2z%yShAmlnyBlXAy$0DlV7g28t
zy;e3KvZjrA)ide05jeCbp)wI**6pq0mNqsoupiTFyt6_X)#$$R!S|jom&hrPTMAP1
zT=+8l44L5Ny>erYaE^kfWzA`4*DG*Hd0SLD({Y}03^*1<6e#X{bxa=Y2y4R)l1H)9;I=2
zCFq?H;@oWov49DxG-~1OsO@-=@&Oy(M^W)v8BqaBD5lWZTn&36Xw5>r;kxLLt}*SR
zQ(#HYVZxwnpNFndNrQn3Hn6+x7UOyr5qXsy`|lb2%e-nR5+qzQNN){T<)2H8P4w;Q`dvRJ2xVE&vTP%z<(KsnPN(>{VIe-
zi>+So%>O*^ET^td3&TB{42KBqpGv_lL^HF+8xU#HyKxgP&r9Gh1Jw(43FUE|1c)7war2AAvH_CNR4lKzONFw(J^94QR2~uWBK1YJW
ziV1-yKw4Nu#=~>jwzGj?|F4ijHIGxNdS?qJ)lf|XukLQu;E-m=9#P>n()~DmaaD&v
zjw0jel~g@X%d?U-zlIyLyg`C!sJ$RM;Y2?U^&msgY5H*n6+Wd&BK(~ivf7$Z+n-AS
z_T!q)a;n5QDkF&QOSbvDvoBXx-$w42@VrA{oa45R<3T7@?@CR6sw(F;&HqisakjRW
zPb~z*u_yN*o2Hy{{<43!DVzQ3lU_q!sNoE<=MdZsW{g`M%Ti?AOyoUbOp!VG>D5A{g+C!k0|Hvj+t
diff --git a/img/onboarding/notification_blue@2x.png b/img/onboarding/notification_blue@2x.png
deleted file mode 100644
index 0aaf19be83f28b79a86192cfcc4186938531e705..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6256
zcmdUUXHb({yY@rxh_C?#5fR)-5u}N9Fd)51l@Hy7j#LE!1pz@wq?gc(bl8Xp
zhLQxNOG1$nI)u>6iGRL%=bLxG=f{~jd(Mw*o>}+InzgQVUw2vSd2M8%$wbdZ4*&p@
zwwAgv0FYfLA&ZupWSLwMy+ZoXd1+bt0szOY-yqXA7T6{k$$X799{`m@*YKnPrHh)r
z8UWNJGo0E}0l*b1ZFMzMDA~r0;d7gDzup~?F@G(a)WBW(_!`p;%9oUHL2tbrZU~W&
z)EfDjP)oOHnK#UnzifY|k=y#^vSlNJyr@jf;fm{XL0k0%f#^|o>oY!GX$Fy1mNyNw
zpHf%^q%>0OsUg&k)B?dMuZKZfgWNv%w{~35LsYsLdidnh9|m>G9sE4mTzkt=+uAy9
zeOwr(2$n~}5gw?g*xr6v${CNi5)DiJViInPK*Ys`%$!JdiicL-)8SRw3A5AG?2WB|
z#r%x5(CDaqJ?FH2_r47>`7*654c7xMEiO}T7>IY;XKs)C895WDqSzB@)!|x;(Cpi1
zSx?B^is$Mc9XA#6+TM7fO%XWXWm4uSZ37bvU1tZpPxSGJ6FlyQK@
zS5OL(2>D(-b3nBpzO?EDXV7`3&nihi_pNLFF@(C|Tmmg?RxS?Z4v|12X
zth(_F#Mo4Gl}>bb8LRy6tdN;3>O0n`?9WUZD$B=r`r7R{9nZmPo6Da|r6ai3sf_Zj
zJOl}{O0Ft*+=Z0vyUIl*)ziz@&dTY{{%mU$Vi0@c(eyO1LbVT&;P##Kb-q29w9r8P
zj=9@|$BgQa;O+NWJ$8P*stsEh`&8yk?1h)Ea#O1|8i=hI<_iC;`KN)Pqr?3`bfY`f
zUR@K#?Xg+!Tq)6DGXvKGHz%pqwp%Jqpa~#RHCSDok=Xa689XjkHpM%PK5AxwJH6Hx
z?J{w@c2R12)%@A4-r(YlS6Oa4E3l28o4~gUyD&b{)Wgbn*9^f;R8Zvd`ne
z;^T*&oXVYVh3vh|`vkBW)=p{w*Tw_K?O
z;p~JuIWn|W{ZR}c#h|Q=Cy&czN4G#$h+|N@i3|@O{|Ubm;%K3a
zyd9l#B}n7}jq3|3Lg3NZD6v}nO#4|%sk01?p%CQUhCI=cDJQ^_Osoq$q9ASS>v7&e&486!(US6IVB^_152s3B%pKk6l_U`#!80_*`2-EZqeN!gb+W$XJnx|--&@+Pg`zg_Phk%d-NSF5#tep)H>7i
zrhY~}guDxtT75g3h~)j?FR?Z!XL_Tv-meRTZx**FpN*QC-=f$a;TsQF5-x-7g_)tk
z*ioEq@B!TP`;RtG%r~MpZv=fP6O@L22~rsGWvl_Br;AtVY)v0m%-46g}Z)}-w7Gt@`9GMYN>Na>t
zb+*e4b41q443x-WMuwlIuaR}q1#^loQj2$qPPyOx`3^@UQ^~w&k_$AwOM08{3#s-n=+fKT7
zp&{Mnfsr(-JRasl(Qy`vh3v6#;>FzrYO-tCVQ~>F>2|Ey05)FGXaj9ZZN$4l5LGIt
zg==3HPKx6`|HngPsva{v8_(?5L7iOpZd;@z#YxSZK~N<>m;855V0gnnx(!A0-=!u~7{wKAHO~g^w62jqZ4FkE^-L?v
z%x6#T?CidauxD)ghPPfgM`L<KXn8cQ@PlbvNC&a2(?l!cD0}q47(TYh?OKMC2DY
zzA>=whnn2$xMlC>6_Pv8lWm|~yz(X$!Y7NJ*tTIX1pJ2KtxXiWYg*XGJpcB#jO}Uelo%6IfnKazlU07zh6fE8l
zesJQaH@Mbej1}G)0N4%rOUj{-8-B7Vv(df^5`VgL@BN8Q-BB|n6p-76IW9=
z42STjF+%+a*-mefpJop@%xiHQci)jk&qs2%Bo-`$_FlS|w*0h3>NPX2&7@klvpjrw
zVF-l!SSnITo%9)%#AT{ie38CpW2NKu(0iS^b$U3%wncdOPDX&W^=A(==m*?kiTtr7Q;Y|$>
z4~T+2;{%&j_V-!GOtDpD=kbCCkjZa@p~vQ-O6L)6Z1#wBqUvSy4Vk|%kE=u@Ta)Ry
zjJ-JfEZ}SD33bMtIk94K$nEbQue#yaF6+#q|6e70Rxq
z5x4M+jj`1l-*lmr~=j=rW%++W=V@h@@myEu21nNByp}6^p^W%z<}x_(ruq#
zhxJF&?6W6RwpeM1E+XsfWmvHs^C;L+Q+)fe8XnmJj$b205$#s;gOT2Bgn
z-lvL9n2sU&k_J1zP>xGzWWJ_9Uw2E7U)IiV{y7iB6KGI_!Ng02QTk4{*7?C)<3-&E
znL}UFGN|8+3+z==(BDI7uC;yByFl<4pQ^NeaSkxN)-^JJ?l2KPd`F{c?*{bj{ZJ)-
zFH9*?1(B923*B>rFu`}L4OCXW5U}_`PODbdez4lB*j)1j!lpwBC_mqol9zq!ZsnE<
z@dZBDEbCzPDCVN@^(1Wx)anDXQ`=g&vr0UNyu89APs8HwGZc4Zkld>Pc(%ezVf8pW
zT@QbQ4x96%cBNELEaJ>u$=>8bvwcp<4YXZ`C9oiAJmkz?`Le&(&P7^{=F8DYdv
zxn*i{?bB#ai)JFV*%9>FZ5U!AAKjkY7EI3WxqguN-gCut@JaY`t{ug-#OfCk!s4t>
zi69V$1>+`Q)-BI*hCS%OSa`pV5fjUfe-1p~bTh
zTsW1NPmZxX!dV0vSO3v_X#hBRX_t*aZa?kdCf5H~|K|7PNQ{&7-9
zVTRs+oA;s$`u!WiZnLy$(%$F@yQ|@XBHop$LuS=359Q};u*kxRGte>y`oelUb+pm44$LvedJ4zR6c6P&5G+Swi
z}Do{#Jv-`hT2t?{`UAs0#WkD>V~Yn>E`F7t{3@(}NFb$yE!7!<1ed?%B}T2iTz
z53#1eT09Xcn7002c4!~@>#;E)-HCx@D5UtD9~BG?c=qiP+kWltP#TGX?l9$WSHW*U
zA~{aamhKPL$Z~0?6UODJPL(DYU)OS_kV>uN017L~v`u`2w
zq(SNUTLbmUg>h7`g?+XM#60OD_TO2$N6$8Y!LgPx4
z!AYoX>EM6VGi(Fv=-|tw!Sss*i6R7VHSC+*sYf-GhGL!k;vK1lyJS{B-XI|0AO(@N
zqm|Jl`9CEv+s)8Bf)*UIj;l6RZwlBXgM|5jtkj{$cCD?qRIIR&J84kCcMc3NNuGdB$g@~7`UwX@6pqdv^-$+;Lq4p$*>k{>XTMVRbOQJ3*DtI^PN2O{Yuns{2eH%
zF%DdKUZt0Gi%ONitsl_LFSHCdL=K$B>e-<7Fm_KT-KRivN^dgBg~#{uyxPC3OYA{hw)zWd%0)
z>&9rid}YOfsqRXJqGeJC*wFEGE9>$4}28Z_ane%AiuzSbZDbN_fZMW)Fv=ll-|txQrlR#Kb^L_JJP?{97c1@}M$vqxBdC
zolYT*jQas}5linar_%?bD&{qEy=~x&?AF@_DLK2}Cc&B4bm=3OWV1GXG>wN;;O{e!
zm@+$H;^17CxW}9fsPAYTfJxaZwvraaY~ND8Nwv=v|q6sPblE}xU@U6hN!%pG8=_bYy7IfRr!lr%hP
zTm>mgSC$q36P3TC_)pmrt|^L4z92&@r(fsz?f>5oMZlD%bH(MzWe+e=A7>jtx*Tem
zlK9Sn)tr~;ct7#jfCu_lkCW8db$tVmk7FILtBZF6(v<_fRDn{lZkj!;ip{4J=N<$Mc;)em3QyO-x1vTZDXzAg1_mrOC}TPckOTn*%(hHh-8pSlK)
zG?>oksFMO62{{cocQXJ!adEJa-__g({w+99cE>3@5Eq9)
zjtYo04_SRBrkKqK|6H+0gXC*1KVv-bVm=ZPQlHFC3MY&RhO9o0IpEDDO>t>b7S?L{
zorPbIU{FV-6Ag;TGNTQsjWMXe|3u~QDE?D+PC2M~?KJtPP{)5&M(?l{KP76W4RF2G
z+IsW5j!02@C_xb^0U{;z0E!(j2PuLGK?BkZ
z0i;6$sPrZYgc>3xU}%Aak_1R@e8>349p8^{jQ8*T?vFV#_F8-IwX^no=6vQe_e&cq
zGs#2phX4S8L7|X4
zW>*1a{fczKfvEQt%PRmt1xkGP?moeFXaBx_#V$f1A<=Gg*_=txZCEON9oOdbGJz6+4$c=B6ugyQ-txKkdlhKmw&uK=)~B&K*Cc#
zIGrAC{PWJlpG}EZE3_VkXbV3)`K0cae)0v?&3*P)?+>tERc$W3f6Jc=>7@1yh4odA_8vYbNvvR;Inz*=Pc{vQj
zX7AX?u58-#ba}D#5HB2+H&Bb8%8fFYt7&yw
z&sQMazv6r1a!kt(x>W}#VGGV6PgCUEVxs6B6C(S{or8Nny^ZbtutQ@~s>1TveXb;_7xD|UN6DJ;k2bX&=MiH5a<+7b@4VCeQJ8J=
zG^Vu*U)Vu>@cSlI!cruSui;r#8AhCR_PkYP$Y2&K$EbQ~*(*B+BPpyxbIHgIv
zuB7xNuezAMV@(M2$z#`Bgc$5RH|6Y#Tx%RR1sw7y)N?pnQ+P}NUzukIA%})}
zGqWSM*GNc%%LXQ?x~!GDxu2TPA2irM$=~C+q#Qn!m-^vks6n8#1j7#+u`Ru_dnulS
z3t3|>n3*yBHB)m(l91PVh>%7Vl1%eGtXJ{?qzJT
zQM?cHmw)JeM=rCoKlCBBY~zRdi09UqnOO?<7*6Xbw!jJWJ}{ufsW$OOX1{fRtq3CU
zUVqWSQCXn+)eJLl{_aS4!8fs2MaV$M@w~2?**JJXHBRd=Ho0BhnuT8{DbSly14l8%
zk*a6nLt)pqem&v^Uia@wYPzTgv4A-=+s5pIA(+}%hljG(73iDNN%;T=RoqKZSh*>3
zM`N!IwFHSA+lUB&RK4;@{^WuwwdZH|d@wXUhvseMWDFi@*{>(ME#Wen)Pl@eOv^;j%8f1jxtgBcH$J5awrwVz}+4q}2-H1NS8{
zWr}6!0_~-!DhC-&m?I#F_Hg%SSL?>4r$!k90djRqb~yZbk4~0%C};}vGWA
zG~4>T;pN?EU_V;zqZQE5*(ho;QqJTKvv5#r+}`zaKvY|*^r}^1>^9Fzt#?7MOuwPp
z0HZMo@pXxoB9FM&`-Q7qmhMJ@yOvy)z7AKftSQ8@yNSu)9`7r+bki(jK%;4V#CYKk
z;OG%N+ipBB(D>YP_!|!)e^{x+2qRx|+@>m{|Ig1cpF0`+0)=NnHHnI448pkyx&@(b
zC>h}rUX?+v0G37VeIJ9ZEVv1kBi}cit-VtRbYARe@V}pLUvGPs`W&&lyeZnUr5je3
z-rn!HdSqY;0<~PKD)Z5jWZbmKQbzJAx?c{)ulQE5FRjnQ=)~tNd+7PfDc`SpW-wo;
z?QDD8Lb4pdAFf<2OVqcr4j<24?~DqQDf7kMI&jyKXu@8SY*@*{DxzC?E{*}
z-j^yc>=l`KrZlwHBUd)mSO!qrv*fa%rmj!h0}rM{En(&46tb$lPNuBr&%j9Jl2OjK
zf$xCma-oLEAEV5`
zuU6UTiRxH&0d$;qdfaz(OwV--=Y46)UZaIyG#Q7in3q;Pj!Z)=KJJqPeD+_?X62|W+)J%5dF$vpvUm)8uV^dFE{NOK#R`)UF25M5Hk~C+Za(2zkm5kQ7(UP?>;dYx
z?hQ9Qg_(Vpl>X7aAs6x0(+uELXgnST`^Xe!>x8k3hX)~sLYPp$waM$Wn7;s@eU{_w
zP2r`aBXcvDir5dmjdum9alKtuHn71<%qYD
z#}+8OGPT`$S9}G~Fx_jdA$A2_+%Q<@mv3))p^?psv`qknw#{wySy_%jx4KGx|0#l=
zbNz$}fc*K}k-NUn$rp%Go|8i=07?Zhx!nuhqz18AaxQS8PzC!doENRV_c1u?A>$5e
z)#f5mUYp|@4T9BpfyCoiyIiO`NT;!kXt0adU}f&U%brsuQ|Yk-+FO-AD;gYEL%!~k
zGa%H_eWbw-zY#j1QlbedNQA!iPO1X@B7_X9d4{@^AW=uUKo4B35Mp^iPAs-W?AjFm
z`0Q#5Ikysx7%aO2@Vnqn$ufGtUw!RBk0M8dodH9?@X~Mxd}xQn&(~Hu+Ks`(=Hl^D
zpUaQWl4|*VltFcTF`H=TB_!nW%~10iC5W+B0)|+91>{*NN%d<{Ukvcw)+Q>P+2?dFi*QNWp*kKUtdbW^zUC+jqWy$+lm_3z{c5IPQjprzRml>IJ4gv$Ub{bGafo)Q52)lYXB
z9kP}f-HZyfGHbSLfFNp)`C4kDTDwQSOgi04W*%wDjw{C@*WKaemEYP1@NE3RCuQRb
zwHxc$lSD7~#$Z
zVh&dsHFT(Jt)mpv^svMbG1JN)Hzk>E%;7i16^*n+c6t^E#s7n$Je9IPoN-GUNiYnR
zPF}vr$7H&*7d1#OTX_q!W;dy-pJXJeY2E+1+&?XGk*6?QMG`CeIn#ADO&W}8EP=yDd
zNgeqo`yc+^9;dmO2b`01!tEgh+#ot6-AI(x|5dHoZV%KMTsL(F{2FdK`lD^J&(8W{
z75^Lk5-6f=L&Ytl5L5DA^WLU4k2SotT5pxe;Z&1MeG72-;A%V@IzQ3Iy$Q7R*NYSZ
zkKN#fJ~t1aD7BV!EQ97;iq_gAqg^d^33uCw~zPT%h%ts(!e?rC_f97-^NL!Wwgk7
z-yj=GgX)wQ=Vr@Y8}j`in$YsbAl((+xJc|q4$q;%BXDbfQi;!l4ndNDrI@*TEd?jE
z+1=LBp_fD<$C}&_1ot|{0hPdyGzeqbaV72d-BySo+8(f}`
z8dtWDF-0HK8^4Dzob*Fge!E+5S
z-+w3uJM%aTr7^B)5Bu9-+%g~p#|6H}-4!t(Mmlq*3`dK0p~3Y2))+bCA^KZAWA3NL
z{ge*<^~cWDLKw|*1P9VKFG{P!=ZPN+=Ta=j-gRF?Pc;4e
z^~Qt*+7evXcW|?SN5+;eTcAsI%;MWWE*d%REieh9pwl>*l1aousM9d`p9vZQje$0V
zU(fJXA{(779*@5r26fJP#x4Eivy^qa{(O!yFmho5Z0fDMzf>2A;ksiLu}qmMP{tGO
zEbORCU0hLPVWLh?T$r?S6DvgQnPaZqG4--yuYtO5+t+X`t3KFX;jDXX;g}D|rt;uP
zxSaf@vuZ^i1Tq@3GJ>rXh*)ylS3u-{3DwY;9cqy$lsp{sRTYpLPK)
zPCW(*_STn7)|B~S!_yNh-j^0){`>fBho*t2X(
z(rv+J7l-VBD`~HJYPeq1X0!cnM2pl_Lzn3*|9lejZO>X~iX%)E$|r2W_O`YOlB*&`
zffrYl=HA;=!7a*PtmW0s&_
z>tXh`!KvA6O-~A6!*s}v2cFAxGIslcZshvf0V%6Y)Xo3#?>#OT7Di%S15E
z%+Z*$7)s|lWErdXRJ;oIT$F*TQ{&H(*ZRI^ueAK*_IL;rYzZT0g!?hut>O7*R_-g@
zS$OaP>bpz}wSfS46~f*`c3l5YN{Q89oGQ!1qIUJ4^kAnGPmcpWMlJ8E{^VipD2&R-G4G7c>kq#d)ytE
zL{DM(TWb(`50tkZX>@&{*bo#Od~x`bp9nZrI%+xJe{b!PyuO9vsAH^eF)O-dO9is=
ze(#jG1etT}wZBGSOP$-;mDX>1KzEm|-Li&eF@Nb#Vsrz{DK*%T;0%HDaIT;fK#moD
z$g)M*_>$t5$^{piYC4nOZ1_p1hamPAgh6R{4vyPyJ+AjY*^NrqF$$W+7jv~Uy;{3p
zD@{%CioRtZ)%S4(RnGk`!s*wxW059TVZomoZ8Hs?cu^BHUWeUFFSBb-8MozJd`Nz&
zE4smc#6-MfnU*r7H}*ta;xdr;8g^NzJ@EFz0W-61eio-jbo2}ivXN#Q^Eq@z1+Q0+
z*U4N^>%r`?d8_bt_6UZ?@3pX99lSStJu&z8rbKDeN=zdh(Amtd?D%M06$*9L2d)1k
zN3c)fyTB4r?TKdHX3hw>(SU-)Vw4s6=W5PM`J|R|i;)Y{e8;NC7A{8$@lrz>?@dIL
z!UsaVb)sTtZEA9>#|j_+FcK{Bu<3xq*cljhqQ3{Il7)KJFmzY7gINqjbRNF+Z>Gd=
zr_=QqTqDE>`zL!-$tOC1O6EG~#+qrjCxRqAtRRN+`rvXsaV@yq{d2^#VF(b8)@kr+
zWHQez3P@P~ui!K$qrkr^J&xL}7lyb-DpEsFx^UWChg2x{EE9jku6_6LDz72-;a%F4
z?XMem%&(Xpb00x^aa!NU@fs88zkE1jiLl67fW~31tKFmsMjXSDSn-V}*KJ&|UF}$Q
zdNmF0?XdYPBWAz>7-XOG26{S9c?uZ_<%YH0Z5433rl7piThheL))^&^S={beXwV`#
z$Rw4-YD*i&_K8O7UYl}yxU(_Dv`u9BGQ_r4Zc0n6-ja?Bbc_xkUpqALn7h~fp0bhP
zS6dIxLtfFZb4U^<-5z@n(n7QHzrbEt4-SC
z6tUK)d|H@SYNV6rl*46V8kAj1}v|O+*z+(=~msg$(V2XvHbVE?J8va-B#Y&%O1Y
zC-pF@#?{UyygP81t&w
zQ(P09T0&b}o3{ltB3AuM4%30&RDV)uS^1dNjqhPc&=Xnv!>R9XDnbljT!bEAosJ*7
zm*qFJ7!$2(Hl_Ge*cr9U05unP!cbxp3LYjL_gmT^_rI*EO*^co8~yqE5}{Bi^rkpf
z`CrIB^F+&3oiNTUF^$w*`(_u^Wh6FW{nu#lXJ4T|p85RGwsxeijHKkUIW;m=<+q5o
zqCB1=CBe7_@YfN7MA^
z^f-youm2$hN{hHXGc9B!A{2EySKWAX$iwgI%3${ZRFCnZV
zG5EjNlEyKJNbUpSyzRc4#pbCIAXY<{bufM^oG!NsWx!D637L=kek{o*qI75V?@;_u
z(JdOLx>sW$;JUpT$UPX?4nT0wmZy0V_y)q=yMO<3Q?E>J2@h~&x9mUaXV
zb4N!0r9&CR!jzB;_@x!8wImQj6^?s(B~uFZ!U)4oFNJ)-gff&~UHCL%C~x*gvgtb)
zbDbEZwLEY%3-ID!)(7Vglbyb0ugIGoRz=kT4I-=YK4O=R;a0uXxLMEaa-$}=pgtHQ
z%H^VOA|_0$R6!h%IPUzf)eDA|J-@^~#JklLEc<&4Lu6QV+DSWIY}5#j%e@0(^UOK0
z?BiO1I+sEpgR^q+XRnL@XC{xl&_#K+KP;bNZ1@k*Xq0MVyxmeR5+YRdZui)m_2Q=+
zn01G!y@dL9AlvDItyFK{qo~c|=?#G_aWLM=%B^nGhT1SwT_DwqEIa0mkM>7<2#Id?
zet5JI8>BQa!uq7QGdH6eO%w>QrvdjG<2<|#BSX(h%Jvwm3AA5JU;q5{L`^3E{uFd1
zTQNxQPi)h(&8xq4zuF@`);H?5s&FhzpE*YZ_q2$Jmj27}?rDXGtU-F2?`)#0f{|#G
zq1}5*fWF9O@x7HzY{M{5LlIeSD)eK5d2#bJxn>bXF1jt-687Ue_ExnSEcE-|w*eEa
zBG*8FXepcp)I8t2CU6kR1i*o7RsVYpv*qKKOUMHJLVm$=aog2qOWnOO0x-F8Z|sfi
zjAJ&pp7;oDG_=jk_Og?I;|0OYaS+Vsa6Gh!v0FZUHL}F|m)tm2fPXGY4&od9;KVRC
zg_1C^`|zS=t$!!r*9{Igc4oPH=n42^`qB;&g;N7WS-AH^(U&*DU}tB~ZQtY^xSqp3
zz`GROlBeXVTb+*HuZY{TsCikUIFy?DOe{>`Jl(dBBcJGz*B&&sTYv9VEfcW(0G)B6
zKL)a8LROa8qogE$}k_y_y0Q~l~stDWQ?VWgoNa|BmFLcVjnsRt7O>91%ifZ%OV1bMMBUT
zUdA@
zmv&NI_A*p|oDJli1}D;Lg>!Xk=+%HUE&A^0GBxqo&Cc|FRm(#pwB^?JHNh5Ts$d
zGAOSUbmvG**jkH?^!1+o67ef{`8Ina(ce@6U;ohh-)lu6h@kI!8W2VGFXb&ER|Xr-
z>!Jl#HhS+8lVY+dFY
z^Tkq1s^dgr7~nf={=4grp-Iib#&0{aa&l-bYqcqYhTqPm4xq=;B*R}GEiyFHCcng@r{UA8G_XDA4gNnpjqlc7CEBjtx(F7>3|os
zp{Fj=n^+BZm&KOJ{|9;dVy$gbsfLfZ4;NOc*%1r-lxikM@~G$ynOWW(N^1v0>N_9r
z`{BYw;JXexst$I;oOF%|8c9(W6}xs%VAeZ^)FOBs6R`V%GmC}?z5S#Pf(
zJx~jvLveT<2m-f$A$u9;qG{5fL^c177d5KyDZzY}^!&f6oImVDPIeJrU$|x70Qn(Z
z@M*&xiu@Re-7D=)3hj9&Az%zRhJb5WSo-`0sVC^{v7-d!Vy^nLp!W>FpO&r{h9-}4
z96V3$&pXa(Fd_e(db)Eb;kBF|&nNH%er*%v8{y$Ctn$hDRHm7erzE@vwtGFvcqVFY
zZg}gX5@5Z_;M@q|H7eli>e_!-c*6nwAFns&1TC9vOe5)6r#40>Uh53n|AK)oTF|{I
ze4A*~SGDNYytriHJH4Yy!v#F30{jfj`ABPV{r@kL{`#GjFA7}-fFXbUH>|FgUG;eUUp~^lhX4Qo
diff --git a/img/onboarding/notification_white.png b/img/onboarding/notification_white.png
index b3458b5a9a62419e4a7cb5b22f71b602c381f6cd..d719c33999dadb3a6d7a978be09b8a726319caf5 100644
GIT binary patch
literal 2474
zcmd^BX*3%O77h)yR7q1Rm14Ts!dR+8X@jaFq^MFwtgm)zEn49*efC-&A(qnGUTsn9
zONH3g-YSi~wNwzXCiWy_PXEn$XJ-D-k9+UA_nhy3_kQR8xN)YadjfnH`2YZbfT4k&
z`AJMUv5!35C;oJf+vStM`_#ZX007_z|I0W4*}0-8MUDXTd%A$CA?V6U0(8|e(E$M7
zC-WaWa-9GWhI%>{Asn=s8Q%w#ORZbqG%)<#rDnzeT!FldT;HSXrT`O*K(VIpPKwRB
z4erkjsCMkEb&s-7!Cz3TW230wf0O#a3(PJM<`TZEV|Mv`c3MeMYUSuwTzJ^)g0Rkl*%`_75ZkKOh@V>z0S
z8mX?HY3f%ahwGj)m~6BgvpIGnVLV+D%8YUz#sE+s!%0{ZWcbr
zf~%sVBICQHqdDoVerJJ&rb3^x^X6)}!psdP+7g9<=>t~-IOzpT6Kw@x;9?cQ*l{_D
zf+2C&R37$w&C6sMaj}UX@f(XZiuC8>0<41qBEcj(;^8enc?SOrwD?l*%>WMr)GP4}
z+Xl?YXppDMQHa^PR#vK$dp)H`%ThE2c>&dTd|YEox&;YkOS}?U9Q%C5Tz!PJ)sn^z
zk}w|ZWyaih!S84(?n{UPU98bw`jXO_sc^-7-gf2BTG{B78fu@zZL0{~O&j}fZ#_FVn&2aGYeIBqym@&QIDFDtdL(|LUg>LJ<~
zS|Q6m%CUKbZ62s|R;(nv55=MK5kW8S(-~9A7w464=fAkT!}|S6Nyb7>309^jnfpt#
z@t{@`abtgc>wCkPh5IaP|4H~FPW}32KL^39Gvmhg2W^KFvGYa*TKM{Gj(|^0OcP==
zSN#@r8g*ExpZ)(Srl&7WwBcTqc`@w}J8%kjP?%Li>(AD>aStn~W!%4W3FaJ#h0Z#qrm-#r0b8w|Rf8NM6UN!SVt=k%
z%_S&lI)aiKRP5H|k^c+rUrv+CLCduN7E6mexl=AF5qO|t
z)fp2yL|S#)TwX06Y`mR^9Wk=*zP~lO|L1j*%SKy=1_42c#wWoCP6Zzoj3dT+Uyk@w$Di;|B4WmlQ{xVNQz!jHs{Sw7
e{@YEpoXWXlwFZ5BmwNJA0Sxb=^s00nqW%fuScSO&
literal 2709
zcmbVOX*3(?8V+g@ON$z!BwE|hP7OsEp|xF1Ns3ZSZLQb^wS^)@Ewxq^EkaRSt+j`!
zwPP7XC`whRB|;N>gII2Q@0|PVo-=pukLP>O^PTg(@B6&ZIq&(N+=3hM0!4uU0D#xX
zP|u7RQ<>=J{DFBAwdxF*fy>v>+8+Sm7W@ttKz6PO^OD8i%s>ZFJt9V7k~6N_CfWeN
z+f?o&Cw2e;C~u^veH+a}{fsewP8a9cB+x_VZ*AHL4RZYoY%>)7%xnJ3~Mh1CB;Tz
zu2wl;&TdUcL|$8w7%Z5uwP1iF&(-w$hSTtp?aibOG9zr`Ah%Gu)`!%5ROESw;f8~*R-iZxndwxe4X~6Dhb7oF
z_h;kPks#gRzTdksjB!Y;JB!}J^zmM4;;>QuSY47w@jXePGXzIKXI8NqFrc-8iw|%9yBV;7g
zJkdd#TiD<7DV}=b=^DG7%3%a00ybp*4jTpRTGCcueqdNtKBTao6LLmfSI)vw{;~+q
zggJ(E;QOYpnCC2q!D*RvKXRGE>Z~TdRGx57EWUg>=GnNoe|OJ&zjuQ^5|7U4s`y=9
z5z7njbg;6j_~IJL@?uPbMPFaP%>{x067JaM$&HsbRd6|~77w~09E=v;mXxe6l+FT5oyW3RFr>XW0JO9W2U#laY2xPt^h7+?CbMN=G{Si1+H?M{;^*&q4NBCIp(
z61Y0qYkKMLv#3F6%PRY18xAE^Q9`14UYI1Lh|l?Ip0EbYG+X}SBc5gAYdc6E&_%aO
z_7RV?Rvb#}WZ#d}q8OVV{Zz5t&IwgD6FS{uzIpwF#xFi$fk{sPFoA?vxevH%Nbam>
zWmZZOK&4;sTq+UyPM{^}g1D&NWVVN8bsmD4-#WD>G1U8OU)Ks~c{@^mM;r(u88U0A
zaI#H5#8sDud)?qz>71(>Ez($bGdbFuZaZzcQ4<_6Hb07y`&16Icv>30Se{m@AGb2v
z)&7mjefrLrZL0gw;pF(Xg{ihd$7iopR`F6efh;Gt*%|6l#!ZxkqLAlvv+}Rs>wtgR
zJ4pXxA+)^P|IPdSjF}1HWXWQRY)w&vh7jF8_=wDjgrF1V=XuT=tWIay6Mb%}QQj>Q
zVD2Q$R?p=R!R6N%$;&tDpmsWacLW>WvxRiy+hkGq!#s3M(MG}30@7U;xcql(zLc+gnH2gJ{I*UT+
zlS5DSCDRON`X+nXw&>?C*6-%;9BLs?t1rmqY~jE@7K47!AACyjJ`!4&EG!~gu~VRO+D|b;c-dc#AF%dw^gMF&^^vTgUuM+;QGdBMk*W4*J9%j)j72ne)Hv#VFJ
zJPOLkl5hnJ{e{il)@iU{kfK6lPS6J;4Z9ZOio%rkm2@5T$aOjU
znVsDiYf_G{(4Tkj*W20SGi*l;uaE^Rz_PI7nBB7XKAVbmBCq@8w&WZijerX7p0Nj319+=P`I#$5h=5@{YSTH57Lh6_1pRV2&
zVmMf~qBN6zsppf$J09%aXSnwCTwH^FUfiE8h*SM^ef(!~edM-HS1w3g>FQ
z#~MY17sz)!>B%`$$X`3MzlXIcf-4~C+egFl9-Oip8A^1i-kJ^8!!2|q?5
zWSa8Tj1%cO$4ajM9d-ft>R39K~Ze0D8rAs<^9ZyT)w&4aJz;i)q$Z+vemTY
zs$Lepuz@*V?W-AG(B&vEPfMg7QW$D2M~8TmFYIra+w^h>55hF^86%J?z|Eh(4l*{E
zP!IxBr>CNr6Zxkr7RhqXf?Ht1NRF$>5>AxW$zfXgu84@zj|+FuyevuTf#zP
z#ObLDbzk1I!H*x!3RVVeKpsbd#E@f0z`rW?S3M1>z-EIWoliW``xmw)=(*JP)kBe~
z&7;C&lO!$ml@Y9|$HrPsEKX3jtK#gh4IcC}yP^Npx=!qZyh7RO0WN%T=lP_|+
RpZSXf80o|Hs&yQr{t2LY8+8Bx
diff --git a/img/onboarding/notification_white@2x.png b/img/onboarding/notification_white@2x.png
index f1ee1212389ffc44fb0ce434df42245b9891ea1e..fd6a244f85cdc35f7172b9e4b36c50251a2d43f5 100644
GIT binary patch
literal 5602
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
AIRF3v
literal 6027
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
F{{qa|Q?LL4
diff --git a/img/onboarding/notification_white@3x.png b/img/onboarding/notification_white@3x.png
index 4abe84597d93eb4bfd5b6efbc2fb08a6cdedcb97..31d60439c221f0a62c77f01595ea27c52f71a204 100644
GIT binary patch
literal 8769
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
literal 9046
zcmdUVX*65w-}Y8j{kK&eX^lPYX-!pB&5h_$v@z8b6+?O&Q&2-qX$V@UT3U3V6h&!-
z5=w)bTZ&_D62u&&MNBb=MC9F`wcc;d^?W%W_FnrQ*1of@dtblb^}FuG-m)+e
zIUsQW005DjH?CU)fB+Ey_T1jT59}dNRDpNkKmIoy0s-JR?cGbj=#APPuu~w=+T;(Q
zxLb+|Huid4GrtA^rHKc({}cj%vvW7EU$eysEQ|^~cUs4dFVj00v41YV0tz(_w>Xrk
zeTgCJpeFw~YGJxO*dBY|IQgl!mYl`?w2Jq3R%mR>M{}j~guD!Fs5zB^wf(3=VeBot
zaX!%E<)g%N0eAQRY3RTIaE!r4u9!`8_c0$|f4cIQ>8LVm
zrr$=LH(5i=!Ww^hP}YFQKRp>g$moFSPDxZ$8S+U2l;Sm0t*^3(lE(ngU&JVxcvLxDPxbWcbs(fcO^uAZlXLk&zFdM7tn_$w
zfA@?hUY_@bHF9uj&8$1V=}8YUIJk#s0q08oR)!E5Dv+s4En2Wts%yUv34)A5i={J1
zpgM$&mUEHp$M%dv!N%7MOYiD(>sD(NOGH1YHoMYp{
z^wLB1-`u6ei6}w4g3hVNTyuJ9}Cgc7-ku
zknDJeXp5=&I1f!{ZrVxmF{P+MZ%tzDkOVQPYc`s7y57AEQFjGM?Q=J*9TFoVIem1A
z0^{Jf-f&>+hM;7**Mc5@6S{N%%24*2CvV%lay^SmZ=e(yM1|uq@%*21{979z3wu2s
zoCSgs5WR={6wBq>or05(5rY