From 1ddf587f5c63c6bf5a236da5a96e31764b86955f Mon Sep 17 00:00:00 2001 From: Cristian Matteu <94987118+ChrisMattew@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:24:14 +0100 Subject: [PATCH 1/9] fix: [IOPID-2547] Fix unhandled error on `Linking.openUrl` (#6554) ## Short description This PR handles any potential errors when attempting to open the store using `Linking.openUrl`. ## List of changes proposed in this pull request - Added a `toast message` to notify the user of the error in case the store cannot be opened. ## Screen |iOS|Android| |---|---------| |![ios-error-message](https://github.com/user-attachments/assets/0f177b06-6b88-4517-adfd-14bd7c243d95)|![android-error-message](https://github.com/user-attachments/assets/e0a83ce0-f83e-4f8a-9e4c-c459df3c9cc1)| --------- Co-authored-by: mariateresaventura <90319761+mariateresaventura@users.noreply.github.com> Co-authored-by: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Co-authored-by: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> --- locales/en/index.yml | 1 + locales/it/index.yml | 1 + .../cie/__tests__/CieIdNotInstalled.test.tsx | 23 +++++++++++-------- .../cie/components/CieIdNotInstalled.tsx | 14 ++++++++--- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/locales/en/index.yml b/locales/en/index.yml index 4e1a03a7de4..8c2b33fac22 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -752,6 +752,7 @@ authentication: title: We can't find the CieID app description: To login, you must have the app installed on your device. primary_action_label: Get the app + link_error: We could not direct you to the store. Open it manually and search for CieID cie: genericTitle: Login with CIE cie: CIE diff --git a/locales/it/index.yml b/locales/it/index.yml index 32ec5c05be3..640a6ab0a72 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -752,6 +752,7 @@ authentication: title: Non riusciamo a trovare l'app CieID description: Per accedere, devi avere l'app installata sul tuo dispositivo. primary_action_label: Scarica CieID + link_error: Non siamo riusciti a indirizzarti allo store. Aprilo manualmente e cerca CieID cie: genericTitle: Entra con CIE cie: CIE diff --git a/ts/features/cie/__tests__/CieIdNotInstalled.test.tsx b/ts/features/cie/__tests__/CieIdNotInstalled.test.tsx index 1d5136c324d..23ac3e19dd3 100644 --- a/ts/features/cie/__tests__/CieIdNotInstalled.test.tsx +++ b/ts/features/cie/__tests__/CieIdNotInstalled.test.tsx @@ -1,4 +1,4 @@ -import { Linking, Platform } from "react-native"; +import { Platform } from "react-native"; import { fireEvent, render } from "@testing-library/react-native"; import React from "react"; import CieIdNotInstalled, { @@ -6,10 +6,13 @@ import CieIdNotInstalled, { CIE_ID_ANDROID_LINK, CIE_ID_IOS_LINK } from "../components/CieIdNotInstalled"; +import * as urlUtils from "../../../utils/url"; const UAT_ENV_ENABLE_STATES = [true, false]; const mockPopToTop = jest.fn(); +const mockOpenUrl = jest.spyOn(urlUtils, "openWebUrl"); +const anyFunction = expect.any(Function); jest.mock("@react-navigation/native", () => ({ ...jest.requireActual("@react-navigation/native"), @@ -28,15 +31,13 @@ describe(CieIdNotInstalled, () => { describe("Behavior on iOS", () => { UAT_ENV_ENABLE_STATES.forEach(uatState => { - it("Should open CIE_ID_IOS_LINK", () => { + it("Should open CIE_ID_IOS_LINK", async () => { const { getByTestId } = render(); const openStore = getByTestId("cie-id-not-installed-open-store"); fireEvent.press(openStore); - expect(jest.spyOn(Linking, "openURL")).toHaveBeenCalledWith( - CIE_ID_IOS_LINK - ); + expect(mockOpenUrl).toHaveBeenCalledWith(CIE_ID_IOS_LINK, anyFunction); expect(mockPopToTop).not.toHaveBeenCalled(); }); }); @@ -50,8 +51,9 @@ describe(CieIdNotInstalled, () => { const openStore = getByTestId("cie-id-not-installed-open-store"); fireEvent.press(openStore); - expect(jest.spyOn(Linking, "openURL")).toHaveBeenCalledWith( - CIE_ID_ANDROID_LINK + expect(mockOpenUrl).toHaveBeenCalledWith( + CIE_ID_ANDROID_LINK, + anyFunction ); expect(mockPopToTop).not.toHaveBeenCalled(); }); @@ -63,8 +65,9 @@ describe(CieIdNotInstalled, () => { const openStore = getByTestId("cie-id-not-installed-open-store"); fireEvent.press(openStore); - expect(jest.spyOn(Linking, "openURL")).toHaveBeenCalledWith( - CIE_ID_ANDROID_COLL_LINK + expect(mockOpenUrl).toHaveBeenCalledWith( + CIE_ID_ANDROID_COLL_LINK, + anyFunction ); expect(mockPopToTop).not.toHaveBeenCalled(); }); @@ -76,7 +79,7 @@ describe(CieIdNotInstalled, () => { const popToTop = getByTestId("cie-id-not-installed-pop-to-top"); fireEvent.press(popToTop); - expect(jest.spyOn(Linking, "openURL")).not.toHaveBeenCalled(); + expect(mockOpenUrl).not.toHaveBeenCalled(); expect(mockPopToTop).toHaveBeenCalledTimes(1); }); }); diff --git a/ts/features/cie/components/CieIdNotInstalled.tsx b/ts/features/cie/components/CieIdNotInstalled.tsx index aec42c74e56..577f548fe9b 100644 --- a/ts/features/cie/components/CieIdNotInstalled.tsx +++ b/ts/features/cie/components/CieIdNotInstalled.tsx @@ -1,9 +1,11 @@ import React from "react"; -import { Linking, Platform } from "react-native"; +import { Platform } from "react-native"; +import { useIOToast } from "@pagopa/io-app-design-system"; import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; import { useIONavigation } from "../../../navigation/params/AppParamsList"; import I18n from "../../../i18n"; import { trackCieIdNotInstalledDownloadAction } from "../analytics"; +import { openWebUrl } from "../../../utils/url"; export const CIE_ID_IOS_LINK = "https://apps.apple.com/it/app/cieid/id1504644677"; @@ -17,6 +19,7 @@ export type CieIdNotInstalledProps = { const CieIdNotInstalled = ({ isUat }: CieIdNotInstalledProps) => { const { popToTop } = useIONavigation(); + const { error } = useIOToast(); return ( { ), onPress: () => { void trackCieIdNotInstalledDownloadAction(); - void Linking.openURL( + openWebUrl( Platform.select({ ios: CIE_ID_IOS_LINK, android: isUat ? CIE_ID_ANDROID_COLL_LINK : CIE_ID_ANDROID_LINK, default: "" - }) + }), + () => { + error( + I18n.t("authentication.cie_id.cie_not_installed.link_error") + ); + } ); } }} From 8cf64a942a0ac538c0f6b493dccd1d8695bc29d5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 19 Dec 2024 15:45:49 +0100 Subject: [PATCH 2/9] chore(Cross): [IOAPPX-432] Development Push notifications for Android (#6416) ## Short description This PR enables testing of push notifications for the development environment on Android. ## List of changes proposed in this pull request - Mock google-services.json file removed in favour of a development one - Such file contains configuration ids and api keys that link to the Firebase development project. Its content is normally embedded into the application bundle and it is easily extracted so it does not contain any secret that should not be committed. - Code that avoided push notification initialisation on Android has been removed, since we now have a valid configuration file - Note that this works only on Android. iOS sandbox is not supported yet ## How to test Using the io-dev-api server, run the application on a real device or on an emulator that uses a Google Play Services image. Use the Firebase Console to send and receive a push notification. Co-authored-by: Alessandro --- google-services-dev.json | 29 ++++++++++++++ mock-google-services.json | 40 ------------------- scripts/generate-api-models.sh | 2 +- .../utils/configurePushNotification.ts | 8 ---- 4 files changed, 30 insertions(+), 49 deletions(-) create mode 100644 google-services-dev.json delete mode 100644 mock-google-services.json diff --git a/google-services-dev.json b/google-services-dev.json new file mode 100644 index 00000000000..b2abb40c7d6 --- /dev/null +++ b/google-services-dev.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "100508770872", + "project_id": "io---app-dev", + "storage_bucket": "io---app-dev.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:100508770872:android:3bae755a9e0015bde77ccd", + "android_client_info": { + "package_name": "it.pagopa.io.app" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDxRvOra_wlultFBZc4D8nbtJ2fyNzUCpk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/mock-google-services.json b/mock-google-services.json deleted file mode 100644 index 690b9a7ef7f..00000000000 --- a/mock-google-services.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "project_info": { - "project_number": "111111111111", - "firebase_url": "", - "project_id": "", - "storage_bucket": "" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:111111111111:android:1111111111111111", - "android_client_info": { - "package_name": "it.pagopa.io.app" - } - }, - "oauth_client": [ - { - "client_id": "", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} diff --git a/scripts/generate-api-models.sh b/scripts/generate-api-models.sh index 7868d3a8ce6..401d50fca5f 100755 --- a/scripts/generate-api-models.sh +++ b/scripts/generate-api-models.sh @@ -60,6 +60,6 @@ for elem in "${apisNoClient[@]}"; do done wait -cp mock-google-services.json ./android/app/google-services.json +cp google-services-dev.json ./android/app/google-services.json yarn generate:locales \ No newline at end of file diff --git a/ts/features/pushNotifications/utils/configurePushNotification.ts b/ts/features/pushNotifications/utils/configurePushNotification.ts index 2356710b8e3..c934c3e4411 100644 --- a/ts/features/pushNotifications/utils/configurePushNotification.ts +++ b/ts/features/pushNotifications/utils/configurePushNotification.ts @@ -13,7 +13,6 @@ import { loadPreviousPageMessages, reloadAllMessages } from "../../messages/store/actions"; -import { isDevEnv } from "../../../utils/environment"; import { trackMessageNotificationParsingFailure, trackMessageNotificationTap @@ -75,16 +74,9 @@ function handleForegroundMessageReload() { }) ); } - // TODO: shall we deep link in foreground? - // see https://pagopaspa.slack.com/archives/C013V764P9U/p1639558176007600 } function configurePushNotifications() { - // if isDevEnv is enabled and we are on Android, we need to disable the push notifications to avoid crash for missing firebase settings - if (isDevEnv && Platform.OS === "android") { - return; - } - // Create the default channel used for notifications, the callback return false if the channel already exists PushNotification.createChannel( { From 323996f8e0207da8a60766ff38042e9a85d9a857 Mon Sep 17 00:00:00 2001 From: Damiano Plebani Date: Thu, 19 Dec 2024 15:48:49 +0100 Subject: [PATCH 3/9] chore(Cross): [IOAPPX-448] Add missing components to the Design System section, remove legacy ones + Change `ListItemMessage` component API (#6541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Short description This PR updates the Design System section with missing components. Checking them in isolation is essential to avoid regression during development. ## List of changes proposed in this pull request - Add `BannerErrorState` to the `DSAdvice` page - Restrict the choice of possible icons to avoid excessive generalisation of the component itself - Add the `ListItemSearchInstitution` to the `DSListItems` page - Rename `CgnModuleDiscount` to `ModuleCgnDiscount`, **refactor it** and move it to `DSModules` from `DSListItems` - Remove gradients from `DSColors` because CGN doesn't use them anymore #### `MessageListItem` → `ListItemMessage` - Change the component name - Rename `DoubleAvatar` to `AvatarDouble` (to keep the same convention as the others) and add it to the `DSLogos` page - Replace `badgeText` and `badgeVariant` props with the single `tag` prop - Remove `CustomPressableListItemBase` by integrating it into `ListItemMessage` - Add all the `ListItemMessage` possible combinations to `DSListItems` for testing purposes ### Preview The new `ListItemMessage` section in the **_List Items_** page ## How to test Go to the **Design System** section and check the update pages --------- Co-authored-by: Emanuele Dall'Ara <71103219+LeleDallas@users.noreply.github.com> Co-authored-by: Cristiano Tofani Co-authored-by: Andrea --- ts/components/ui/BannerErrorState.tsx | 2 +- .../merchants/CgnMerchantsDiscountItem.tsx | 4 +- ...duleDiscount.tsx => ModuleCgnDiscount.tsx} | 78 +- ...nt.test.tsx => ModuleCgnDiscount.test.tsx} | 6 +- .../merchants/discount/CgnDiscountHeader.tsx | 7 +- ts/features/design-system/DesignSystem.tsx | 8 +- ts/features/design-system/core/DSAdvice.tsx | 26 + ts/features/design-system/core/DSColors.tsx | 73 +- .../design-system/core/DSListItems.tsx | 172 +- ts/features/design-system/core/DSLogos.tsx | 7 + ts/features/design-system/core/DSModules.tsx | 54 + .../design-system/core/DSTypography.tsx | 12 +- .../DS/{DoubleAvatar.tsx => AvatarDouble.tsx} | 8 +- .../Home/DS/CustomPressableListItemBase.tsx | 69 - .../components/Home/DS/ListItemMessage.tsx | 226 +++ ...eleton.tsx => ListItemMessageSkeleton.tsx} | 16 +- .../components/Home/DS/MessageListItem.tsx | 187 --- .../messages/components/Home/Footer.tsx | 6 +- .../messages/components/Home/MessageList.tsx | 10 +- ...istItem.tsx => WrappedListItemMessage.tsx} | 44 +- ...st.tsx => WrappedListItemMessage.test.tsx} | 6 +- ...p => WrappedListItemMessage.test.tsx.snap} | 1380 ++++++++--------- .../messages/components/Home/homeUtils.ts | 11 +- .../MessageDetail/OrganizationHeader.tsx | 4 +- .../messages/screens/MessagesSearchScreen.tsx | 4 +- .../components/ListItemSearchInstitution.tsx | 4 +- 26 files changed, 1263 insertions(+), 1161 deletions(-) rename ts/features/bonus/cgn/components/merchants/{CgnModuleDiscount.tsx => ModuleCgnDiscount.tsx} (66%) rename ts/features/bonus/cgn/components/merchants/___tests___/{CgnModuleDiscount.test.tsx => ModuleCgnDiscount.test.tsx} (87%) rename ts/features/messages/components/Home/DS/{DoubleAvatar.tsx => AvatarDouble.tsx} (94%) delete mode 100644 ts/features/messages/components/Home/DS/CustomPressableListItemBase.tsx create mode 100644 ts/features/messages/components/Home/DS/ListItemMessage.tsx rename ts/features/messages/components/Home/DS/{MessageListItemSkeleton.tsx => ListItemMessageSkeleton.tsx} (89%) delete mode 100644 ts/features/messages/components/Home/DS/MessageListItem.tsx rename ts/features/messages/components/Home/{WrappedMessageListItem.tsx => WrappedListItemMessage.tsx} (86%) rename ts/features/messages/components/Home/__tests__/{WrappedMessageListItem.test.tsx => WrappedListItemMessage.test.tsx} (97%) rename ts/features/messages/components/Home/__tests__/__snapshots__/{WrappedMessageListItem.test.tsx.snap => WrappedListItemMessage.test.tsx.snap} (87%) diff --git a/ts/components/ui/BannerErrorState.tsx b/ts/components/ui/BannerErrorState.tsx index 62b081e6883..1cf9ad3ad74 100644 --- a/ts/components/ui/BannerErrorState.tsx +++ b/ts/components/ui/BannerErrorState.tsx @@ -67,7 +67,7 @@ export type BannerErrorStateProps = BaseBannerErrorStateProps & */ export const BannerErrorState = ({ viewRef, - icon, + icon = "warningFilled", label, actionText, onPress, diff --git a/ts/features/bonus/cgn/components/merchants/CgnMerchantsDiscountItem.tsx b/ts/features/bonus/cgn/components/merchants/CgnMerchantsDiscountItem.tsx index 40003d9990b..9246d3f5e9d 100644 --- a/ts/features/bonus/cgn/components/merchants/CgnMerchantsDiscountItem.tsx +++ b/ts/features/bonus/cgn/components/merchants/CgnMerchantsDiscountItem.tsx @@ -7,7 +7,7 @@ import CGN_ROUTES from "../../navigation/routes"; import { CgnDetailsParamsList } from "../../navigation/params"; import { useIODispatch } from "../../../../../store/hooks"; import { selectMerchantDiscount } from "../../store/actions/merchants"; -import { CgnModuleDiscount } from "./CgnModuleDiscount"; +import { ModuleCgnDiscount } from "./ModuleCgnDiscount"; type Props = { discount: Discount; @@ -38,7 +38,7 @@ export const CgnMerchantDiscountItem: React.FunctionComponent = ({ return ( - + ); }; diff --git a/ts/features/bonus/cgn/components/merchants/CgnModuleDiscount.tsx b/ts/features/bonus/cgn/components/merchants/ModuleCgnDiscount.tsx similarity index 66% rename from ts/features/bonus/cgn/components/merchants/CgnModuleDiscount.tsx rename to ts/features/bonus/cgn/components/merchants/ModuleCgnDiscount.tsx index 22bf2b746fb..06266d234ef 100644 --- a/ts/features/bonus/cgn/components/merchants/CgnModuleDiscount.tsx +++ b/ts/features/bonus/cgn/components/merchants/ModuleCgnDiscount.tsx @@ -1,14 +1,14 @@ import { Badge, H6, - HSpacer, + HStack, IOColors, IOModuleStyles, IOStyles, Icon, Tag, - VSpacer, - useIOExperimentalDesign, + VStack, + useIOTheme, useScaleAnimation } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; @@ -21,7 +21,7 @@ import I18n from "../../../../../i18n"; import { getCategorySpecs } from "../../utils/filters"; import { isValidDiscount, normalizedDiscountPercentage } from "./utils"; -type Props = { +export type ModuleCgnDiscount = { onPress: () => void; discount: Discount; }; @@ -39,6 +39,7 @@ const styles = StyleSheet.create({ borderWidth: 1 } }); + type CategoryTagProps = { category: ProductCategory; }; @@ -47,24 +48,19 @@ export const CategoryTag = ({ category }: CategoryTagProps) => { const categorySpecs = getCategorySpecs(category); return O.isSome(categorySpecs) ? ( - <> - - - - - - + ) : null; }; -export const CgnModuleDiscount = ({ onPress, discount }: Props) => { - const { isExperimental } = useIOExperimentalDesign(); + +export const ModuleCgnDiscount = ({ onPress, discount }: ModuleCgnDiscount) => { + const theme = useIOTheme(); const { onPressIn, onPressOut, scaleAnimatedStyle } = useScaleAnimation("medium"); @@ -92,37 +88,37 @@ export const CgnModuleDiscount = ({ onPress, discount }: Props) => { { alignItems: "center", justifyContent: "space-between" } ]} > - - - {discount.isNew && ( - <> + + {(discount.discount || discount.isNew) && ( + + {discount.isNew && ( - - - )} - {isValidDiscount(discount.discount) && ( - - )} - - + )} + {isValidDiscount(discount.discount) && ( + + )} + + )} +
{discount.name}
- - + {discount.productCategories.map(categoryKey => ( ))} - -
+ + diff --git a/ts/features/bonus/cgn/components/merchants/___tests___/CgnModuleDiscount.test.tsx b/ts/features/bonus/cgn/components/merchants/___tests___/ModuleCgnDiscount.test.tsx similarity index 87% rename from ts/features/bonus/cgn/components/merchants/___tests___/CgnModuleDiscount.test.tsx rename to ts/features/bonus/cgn/components/merchants/___tests___/ModuleCgnDiscount.test.tsx index d70d95b2cef..4411cb19ce2 100644 --- a/ts/features/bonus/cgn/components/merchants/___tests___/CgnModuleDiscount.test.tsx +++ b/ts/features/bonus/cgn/components/merchants/___tests___/ModuleCgnDiscount.test.tsx @@ -4,7 +4,7 @@ import React from "react"; import { Discount } from "../../../../../../../definitions/cgn/merchants/Discount"; import { ProductCategoryEnum } from "../../../../../../../definitions/cgn/merchants/ProductCategory"; import I18n from "../../../../../../i18n"; -import { CgnModuleDiscount } from "../CgnModuleDiscount"; +import { ModuleCgnDiscount } from "../ModuleCgnDiscount"; describe("CgnModuleDiscount", () => { const discount: Discount = { @@ -27,7 +27,7 @@ describe("CgnModuleDiscount", () => { it("should render correctly", () => { const { getByText } = render( - + ); expect(getByText(I18n.t("bonus.cgn.merchantsList.news"))).toBeTruthy(); @@ -37,7 +37,7 @@ describe("CgnModuleDiscount", () => { it("should call onPress when pressed", () => { const { getByRole } = render( - + ); fireEvent.press(getByRole("button")); expect(onPressMock).toHaveBeenCalled(); diff --git a/ts/features/bonus/cgn/components/merchants/discount/CgnDiscountHeader.tsx b/ts/features/bonus/cgn/components/merchants/discount/CgnDiscountHeader.tsx index 3174b6f1a96..1fa27a9b3f7 100644 --- a/ts/features/bonus/cgn/components/merchants/discount/CgnDiscountHeader.tsx +++ b/ts/features/bonus/cgn/components/merchants/discount/CgnDiscountHeader.tsx @@ -1,6 +1,7 @@ import { Badge, H3, + HStack, IOColors, IOStyles, VSpacer @@ -10,7 +11,7 @@ import React from "react"; import { StyleSheet, View } from "react-native"; import { Discount } from "../../../../../../../definitions/cgn/merchants/Discount"; import I18n from "../../../../../../i18n"; -import { CategoryTag } from "../CgnModuleDiscount"; +import { CategoryTag } from "../ModuleCgnDiscount"; import { isValidDiscount, normalizedDiscountPercentage } from "../utils"; type CgnDiscountHeaderProps = { @@ -65,11 +66,11 @@ export const CgnDiscountHeader = ({ )}

{name}

- + {productCategories.map(categoryKey => ( ))} - + ); diff --git a/ts/features/design-system/DesignSystem.tsx b/ts/features/design-system/DesignSystem.tsx index 55af8532d4a..ae7184e4a69 100644 --- a/ts/features/design-system/DesignSystem.tsx +++ b/ts/features/design-system/DesignSystem.tsx @@ -1,8 +1,8 @@ import { + BodySmall, Divider, - H2, + H3, IOVisualCostants, - BodySmall, ListItemNav, VSpacer, VStack, @@ -109,7 +109,9 @@ export const DesignSystem = () => { section: { title: string; description?: string }; }) => ( -

{title}

+

+ {title} +

{description && ( {description} diff --git a/ts/features/design-system/core/DSAdvice.tsx b/ts/features/design-system/core/DSAdvice.tsx index c5f090a79eb..81b6bc0f9b6 100644 --- a/ts/features/design-system/core/DSAdvice.tsx +++ b/ts/features/design-system/core/DSAdvice.tsx @@ -10,6 +10,7 @@ import * as React from "react"; import { Alert } from "react-native"; import { DSComponentViewerBox } from "../components/DSComponentViewerBox"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; +import { BannerErrorState } from "../../../components/ui/BannerErrorState"; const onLinkPress = () => { Alert.alert("Alert", "Action triggered"); @@ -35,6 +36,11 @@ export const DSAdvice = () => { {renderFeatureInfo()}
+ +

BannerErrorState

+ {renderBannerErrorState()} +
+

Banner

{renderBanner()} @@ -44,6 +50,26 @@ export const DSAdvice = () => { ); }; +const renderBannerErrorState = () => ( + + + + + + + + +); + const renderFeatureInfo = () => ( diff --git a/ts/features/design-system/core/DSColors.tsx b/ts/features/design-system/core/DSColors.tsx index 141b3ffb784..bea71d79901 100644 --- a/ts/features/design-system/core/DSColors.tsx +++ b/ts/features/design-system/core/DSColors.tsx @@ -1,7 +1,7 @@ import { + BodySmall, H3, H6, - IOColorGradients, IOColors, IOColorsExtra, IOColorsLegacy, @@ -10,7 +10,6 @@ import { IOColorsTints, IOThemeDark, IOThemeLight, - BodySmall, VStack, hexToRgba, themeStatusColorsDarkMode, @@ -19,13 +18,11 @@ import { } from "@pagopa/io-app-design-system"; import * as React from "react"; import { ColorValue, Dimensions, StyleSheet, Text, View } from "react-native"; -import LinearGradient from "react-native-linear-gradient"; import { IOStyles } from "../../../components/core/variables/IOStyles"; import themeVariables from "../../../theme/variables"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; const macroSectionMargin = 48; -const gradientItemGutter = 16; const sectionTitleMargin = 16; const colorItemGutter = 32; const colorItemPadding = 8; @@ -36,13 +33,6 @@ const colorItemBorderDarkMode = hexToRgba(IOColors.white, 0.25); const colorPillBg = hexToRgba(IOColors.black, 0.2); const styles = StyleSheet.create({ - gradientItemsWrapper: { - flexDirection: "row", - flexWrap: "wrap", - justifyContent: "flex-start", - marginLeft: (gradientItemGutter / 2) * -1, - marginRight: (gradientItemGutter / 2) * -1 - }, colorItemsWrapper: { flexDirection: "row", flexWrap: "wrap", @@ -93,12 +83,6 @@ const styles = StyleSheet.create({ borderTopRightRadius: 24, borderBottomRightRadius: 24 }, - gradientWrapper: { - width: "50%", - justifyContent: "flex-start", - paddingHorizontal: gradientItemGutter / 2, - marginBottom: 16 - }, colorItem: { width: "100%", padding: colorItemPadding, @@ -112,15 +96,6 @@ const styles = StyleSheet.create({ colorItemDarkMode: { borderColor: colorItemBorderDarkMode }, - gradientItem: { - aspectRatio: 2 / 1, - borderRadius: 8, - padding: 12, - alignItems: "flex-end", - justifyContent: "space-between", - borderColor: colorItemBorderLightMode, - borderWidth: 1 - }, colorPill: { overflow: "hidden", color: IOColors.white, @@ -285,17 +260,6 @@ export const DSColors = () => {
- {/* GRADIENTS */} - -

Gradients

- - - {Object.entries(IOColorGradients).map(([name, colorValues]) => ( - - ))} - -
- {/* LEGACY */} @@ -372,41 +336,6 @@ const ColorBox = ({ ); }; -type GradientBoxProps = { - name: string; - colors: Array; -}; - -const GradientBox = ({ name, colors }: GradientBoxProps) => { - const theme = useIOTheme(); - const [first, last] = colors; - - return ( - - - {first && {first}} - {last && {last}} - - {name && ( - - {name} - - )} - - ); -}; - type SmallCapsTitleProps = { title: string; darkMode?: boolean; diff --git a/ts/features/design-system/core/DSListItems.tsx b/ts/features/design-system/core/DSListItems.tsx index 57ba689ac01..637ee7ebc60 100644 --- a/ts/features/design-system/core/DSListItems.tsx +++ b/ts/features/design-system/core/DSListItems.tsx @@ -1,4 +1,3 @@ -import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import * as React from "react"; import { @@ -17,14 +16,16 @@ import { useIOTheme } from "@pagopa/io-app-design-system"; import { Alert } from "react-native"; +import I18n from "../../../i18n"; import { DSComponentViewerBox } from "../components/DSComponentViewerBox"; -import { ProductCategoryEnum } from "../../../../definitions/cgn/merchants/ProductCategory"; -import { CgnMerchantDiscountItem } from "../../bonus/cgn/components/merchants/CgnMerchantsDiscountItem"; +import { ListItemMessage } from "../../messages/components/Home/DS/ListItemMessage"; +import { ListItemMessageSkeleton } from "../../messages/components/Home/DS/ListItemMessageSkeleton"; import { getBadgePropsByTransactionStatus } from "../../payments/common/utils"; -import { DesignSystemScreen } from "../components/DesignSystemScreen"; import { ListItemTransactionStatus } from "../../payments/common/utils/types"; +import { ListItemSearchInstitution } from "../../services/common/components/ListItemSearchInstitution"; +import { DesignSystemScreen } from "../components/DesignSystemScreen"; const onButtonPress = () => { Alert.alert("Alert", "Action triggered"); @@ -34,6 +35,8 @@ const onCopyButtonPress = () => { Alert.alert("Copied!", "Value copied"); }; +const cdnPath = "https://assets.cdn.io.pagopa.it/logos/organizations/"; + const sectionTitleMargin = 16; const sectionMargin = 48; const componentMargin = 32; @@ -49,6 +52,11 @@ export const DSListItems = () => { {renderListItemNav()} + +

ListItemMessage

+ {renderListItemMessage()} +
+

ListItemInfoCopy

{renderListItemInfoCopy()} @@ -70,31 +78,15 @@ export const DSListItems = () => {
-

ListItemTransaction

- {renderListItemTransaction()} +

+ ListItemSearchInstitution +

+ {renderListItemSearchInstitution()}
-

Specific

- - - - - +

ListItemTransaction

+ {renderListItemTransaction()}
@@ -160,6 +152,20 @@ const renderListItemNav = () => ( hideChevron /> + + + + + @@ -181,6 +187,99 @@ const renderListItemNav = () => ( ); +const listItemMessageSample: ListItemMessage = { + formattedDate: "09 dic", + isRead: false, + messageTitle: "Il tuo appuntamento", + organizationName: "Ministero dell'Interno", + serviceName: "Carta d'Identità Elettronica", + accessibilityLabel: "Leggi il messaggio inviato dal Ministero dell'Interno", + serviceLogos: [{ uri: `${cdnPath}80215430580.png` }], + onLongPress: () => { + Alert.alert("Long press"); + }, + onPress: () => { + Alert.alert("Pressed"); + } +}; + +const renderListItemMessage = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + const renderListItemInfoCopy = () => ( ( ); +/* LIST ITEM SEARCH INSTITUTION */ + +const renderListItemSearchInstitution = () => ( + + + + + +); + /* LIST ITEM TRANSACTION */ /* Mock assets */ -const cdnPath = "https://assets.cdn.io.pagopa.it/logos/organizations/"; const organizationLogoURI = { imageSource: `${cdnPath}82003830161.png`, name: "Comune di Milano" diff --git a/ts/features/design-system/core/DSLogos.tsx b/ts/features/design-system/core/DSLogos.tsx index b6ba74d7e44..17275b21fe6 100644 --- a/ts/features/design-system/core/DSLogos.tsx +++ b/ts/features/design-system/core/DSLogos.tsx @@ -23,6 +23,7 @@ import { import * as React from "react"; import { ScrollView, StyleSheet, View } from "react-native"; import { LogoPaymentExtended } from "../../../components/ui/LogoPaymentExtended"; +import { AvatarDouble } from "../../messages/components/Home/DS/AvatarDouble"; import { DSComponentViewerBox } from "../components/DSComponentViewerBox"; import { DSLogoPaymentViewerBox, @@ -199,6 +200,12 @@ const renderAvatar = () => ( + + + + ); diff --git a/ts/features/design-system/core/DSModules.tsx b/ts/features/design-system/core/DSModules.tsx index 914d8edb82d..c111b29e006 100644 --- a/ts/features/design-system/core/DSModules.tsx +++ b/ts/features/design-system/core/DSModules.tsx @@ -11,9 +11,12 @@ import { VStack, useIOTheme } from "@pagopa/io-app-design-system"; +import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import * as React from "react"; import { Alert, ImageSourcePropType } from "react-native"; +import { ProductCategoryEnum } from "../../../../definitions/cgn/merchants/ProductCategory"; import CgnLogo from "../../../../img/bonus/cgn/cgn_logo.png"; +import { ModuleCgnDiscount } from "../../bonus/cgn/components/merchants/ModuleCgnDiscount"; import { getBadgeTextByPaymentNoticeStatus } from "../../messages/utils/strings"; import { DSComponentViewerBox } from "../components/DSComponentViewerBox"; import { DesignSystemScreen } from "../components/DesignSystemScreen"; @@ -75,6 +78,11 @@ export const DSModules = () => { {renderModuleSummary()} + +

ModuleCgnDiscount

+ {renderModuleCgnDiscount()} +
+

ModuleIDP

{renderModuleIDP()} @@ -276,6 +284,52 @@ const renderModuleSummary = () => (
); +const mockModuleCgnDiscountData = { + name: "Small Rubber Chips" as NonEmptyString, + id: "28201" as NonEmptyString, + description: undefined, + discount: undefined, + discountUrl: "https://localhost", + endDate: new Date(), + isNew: false, + productCategories: [ProductCategoryEnum.cultureAndEntertainment], + startDate: new Date() +}; + +const renderModuleCgnDiscount = () => ( + + + + + + + + + + + +); + const mockIDPProviderItem = { id: "posteid", name: "Poste ID", diff --git a/ts/features/design-system/core/DSTypography.tsx b/ts/features/design-system/core/DSTypography.tsx index 5563710fdc8..5a92bf0a8c1 100644 --- a/ts/features/design-system/core/DSTypography.tsx +++ b/ts/features/design-system/core/DSTypography.tsx @@ -106,7 +106,7 @@ const H4Row = () => { const theme = useIOTheme(); return ( - +

Header H4

Header H4

{ const theme = useIOTheme(); return ( - +
Header H5
Header H5
Header H5
@@ -134,7 +134,7 @@ const H6Row = () => { const theme = useIOTheme(); return ( - +
Header H6
Header H6
Header H6
@@ -146,7 +146,7 @@ const ButtonTextRow = () => { const theme = useIOTheme(); return ( - + ButtonText ButtonText @@ -164,7 +164,7 @@ const CaptionRow = () => { const theme = useIOTheme(); return ( - + Caption Caption Caption @@ -222,7 +222,7 @@ export const BodySmallRow = () => { Body small SB asLink - + Body small Regular Body small Regular diff --git a/ts/features/messages/components/Home/DS/DoubleAvatar.tsx b/ts/features/messages/components/Home/DS/AvatarDouble.tsx similarity index 94% rename from ts/features/messages/components/Home/DS/DoubleAvatar.tsx rename to ts/features/messages/components/Home/DS/AvatarDouble.tsx index 3509fa4bcda..eb1275a5228 100644 --- a/ts/features/messages/components/Home/DS/DoubleAvatar.tsx +++ b/ts/features/messages/components/Home/DS/AvatarDouble.tsx @@ -17,7 +17,7 @@ import { } from "@pagopa/io-app-design-system"; import { addCacheTimestampToUri } from "../../../../../utils/image"; -type DoubleAvatarProps = { +type AvatarDoubleProps = { backgroundLogoUri?: | ImageRequireSource | ImageURISource @@ -74,12 +74,12 @@ const getImageState = ( }; /** - * DoubleAvatar component is used to display the background logo of an organization, with a fixed pagoPA icon on top. It accepts the following props: + * `AvatarDouble` component is used to display the background logo of an organization, with a fixed pagoPA icon on top. It accepts the following props: * - `backgroundLogoUri`: the uri of the image to display. If not provided, a placeholder icon will be displayed. It can be a single uri or an array of uris, in which case the first one that is available will be used. - * @param DoubleAvatarProps + * @param AvatarDoubleProps * @returns */ -export const DoubleAvatar = ({ backgroundLogoUri }: DoubleAvatarProps) => { +export const AvatarDouble = ({ backgroundLogoUri }: AvatarDoubleProps) => { const theme = useIOTheme(); const indexValue = React.useRef(0); diff --git a/ts/features/messages/components/Home/DS/CustomPressableListItemBase.tsx b/ts/features/messages/components/Home/DS/CustomPressableListItemBase.tsx deleted file mode 100644 index 82e72558d48..00000000000 --- a/ts/features/messages/components/Home/DS/CustomPressableListItemBase.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - IOColors, - IOListItemStyles, - useListItemAnimation, - WithTestID -} from "@pagopa/io-app-design-system"; -import * as React from "react"; -import { Pressable } from "react-native"; -import Animated from "react-native-reanimated"; - -export type PressableBaseProps = WithTestID< - Pick< - React.ComponentProps, - | "onPress" - | "onLongPress" - | "accessibilityLabel" - | "accessibilityHint" - | "accessibilityState" - | "accessibilityRole" - > & { minHeight?: number; selected?: boolean } ->; - -export const CustomPressableListItemBase = ({ - onPress, - onLongPress, - testID, - children, - accessibilityRole, - minHeight, - selected, - ...props -}: React.PropsWithChildren) => { - const { onPressIn, onPressOut, scaleAnimatedStyle, backgroundAnimatedStyle } = - useListItemAnimation(); - - return ( - - - - {children} - - - - ); -}; diff --git a/ts/features/messages/components/Home/DS/ListItemMessage.tsx b/ts/features/messages/components/Home/DS/ListItemMessage.tsx new file mode 100644 index 00000000000..fdc0941645a --- /dev/null +++ b/ts/features/messages/components/Home/DS/ListItemMessage.tsx @@ -0,0 +1,226 @@ +import { + AnimatedMessageCheckbox, + Avatar, + BodySmall, + H6, + HStack, + IOColors, + IOListItemStyles, + IOStyles, + IOVisualCostants, + Tag, + useIOTheme, + useListItemAnimation, + WithTestID +} from "@pagopa/io-app-design-system"; +import React, { ComponentProps } from "react"; +import { ImageURISource, Pressable, StyleSheet, View } from "react-native"; +import Animated from "react-native-reanimated"; +import Svg, { Circle } from "react-native-svg"; +import I18n from "../../../../../i18n"; +import { AvatarDouble } from "./AvatarDouble"; + +export const ListItemMessageStandardHeight = 95; +export const ListItemMessageEnhancedHeight = 133; + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + paddingHorizontal: 16 + }, + organizationContainer: { + flexDirection: "row" + }, + messageReadContainer: { + justifyContent: "center", + marginLeft: 8 + }, + serviceNameAndMessageTitleContainer: { + flexDirection: "row" + }, + serviceLogoAndSelectionContainer: { + justifyContent: "center" + }, + serviceLogoContainer: { + alignItems: "center", + justifyContent: "center", + height: IOVisualCostants.avatarSizeSmall, + width: IOVisualCostants.avatarSizeSmall + }, + textContainer: { ...IOStyles.flex, marginLeft: 8 } +}); + +type ListItemMessageTag = { + text: string; + variant: Extract; +}; + +export type ListItemMessage = WithTestID<{ + accessibilityLabel: string; + tag?: ListItemMessageTag; + avatarDouble?: boolean; + formattedDate: string; + isRead: boolean; + messageTitle: string; + onLongPress: () => void; + onPress: () => void; + organizationName: string; + selected?: boolean; + serviceLogos?: ReadonlyArray; + serviceName: string; +}> & + Pick< + ComponentProps, + | "onPress" + | "onLongPress" + | "accessibilityLabel" + | "accessibilityHint" + | "accessibilityState" + | "accessibilityRole" + >; + +type UnreadBadgeProps = { + color?: IOColors; + width?: number; +}; + +const UnreadBadge = ({ color, width = 14 }: UnreadBadgeProps) => ( + + + +); + +export const ListItemMessage = ({ + accessibilityLabel, + accessibilityRole, + tag, + avatarDouble, + formattedDate, + isRead, + messageTitle, + onLongPress, + onPress, + organizationName, + serviceName, + selected, + serviceLogos, + testID +}: ListItemMessage) => { + const theme = useIOTheme(); + + const { onPressIn, onPressOut, scaleAnimatedStyle, backgroundAnimatedStyle } = + useListItemAnimation(); + + return ( + + + + + + + {avatarDouble ? ( + + ) : ( + + )} + + + + + + + + {/* We use 'flexGrow: 1, flexShrink: 1' instead of 'flex: 1' + in order to keep a consistent styling approach between + react and react-native (on react-native, 'flex: 1' does + something similar to 'flex-grow') */} +
+ {organizationName} +
+ + {formattedDate} + +
+ + + {`${serviceName} · `} + {messageTitle} + + {!isRead && ( + + + + )} + + {tag && ( + /* We add a `View` because we need to re-arrange block elements using + `VStack` and/or `HStack` */ + + + + {tag.variant === "legalMessage" && ( + + )} + + + )} +
+
+
+
+
+ ); +}; diff --git a/ts/features/messages/components/Home/DS/MessageListItemSkeleton.tsx b/ts/features/messages/components/Home/DS/ListItemMessageSkeleton.tsx similarity index 89% rename from ts/features/messages/components/Home/DS/MessageListItemSkeleton.tsx rename to ts/features/messages/components/Home/DS/ListItemMessageSkeleton.tsx index 13f376cdf39..b91e632a14e 100644 --- a/ts/features/messages/components/Home/DS/MessageListItemSkeleton.tsx +++ b/ts/features/messages/components/Home/DS/ListItemMessageSkeleton.tsx @@ -1,14 +1,16 @@ -import React from "react"; -import { StyleSheet, View } from "react-native"; -import Placeholder from "rn-placeholder"; import { IOColors, IOStyles, IOVisualCostants, WithTestID } from "@pagopa/io-app-design-system"; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import Placeholder from "rn-placeholder"; +import { ListItemMessageStandardHeight } from "./ListItemMessage"; -export const SkeletonHeight = 95 + StyleSheet.hairlineWidth; +export const SkeletonHeight = + ListItemMessageStandardHeight + StyleSheet.hairlineWidth; const styles = StyleSheet.create({ container: { @@ -39,13 +41,13 @@ const styles = StyleSheet.create({ } }); -type MessageListItemSkeletonProps = WithTestID<{ +type ListItemMessageSkeletonProps = WithTestID<{ accessibilityLabel: string; }>; -export const MessageListItemSkeleton = ({ +export const ListItemMessageSkeleton = ({ accessibilityLabel -}: MessageListItemSkeletonProps) => ( +}: ListItemMessageSkeletonProps) => ( void; - onPress: () => void; - organizationName: string; - selected?: boolean; - serviceLogos?: ReadonlyArray; - serviceName: string; -}>; - -type BadgeComponentProps = { - color?: IOColors; - width?: number; -}; - -const BadgeComponent = ({ color, width = 14 }: BadgeComponentProps) => ( - - - -); - -export const MessageListItem = ({ - accessibilityLabel, - badgeText, - badgeVariant, - doubleAvatar, - formattedDate, - isRead, - messageTitle, - onLongPress, - onPress, - organizationName, - serviceName, - selected, - serviceLogos, - testID -}: MessageListItemProps) => { - const theme = useIOTheme(); - - return ( - - - - - {doubleAvatar ? ( - - ) : ( - - )} - - - - - - - - {/* We use 'flexGrow: 1, flexShrink: 1' instead of 'flex: 1' - in order to keep a consistent styling approach between - react and react-native (on react-native, 'flex: 1' does - something similar to 'flex-grow') */} -
- {organizationName} -
- - {formattedDate} - -
- - - {`${serviceName} · `} - {messageTitle} - - {!isRead && ( - - - - )} - - {badgeText && badgeVariant && ( - - - {badgeVariant === "legalMessage" && ( - <> - - - - )} - - )} -
-
-
- ); -}; diff --git a/ts/features/messages/components/Home/Footer.tsx b/ts/features/messages/components/Home/Footer.tsx index 2d3d5000013..4bae8e24178 100644 --- a/ts/features/messages/components/Home/Footer.tsx +++ b/ts/features/messages/components/Home/Footer.tsx @@ -1,9 +1,9 @@ import React from "react"; +import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; import { shouldShowFooterListComponentSelector } from "../../store/reducers/allPaginated"; import { MessageListCategory } from "../../types/messageListCategory"; -import I18n from "../../../../i18n"; -import { MessageListItemSkeleton } from "./DS/MessageListItemSkeleton"; +import { ListItemMessageSkeleton } from "./DS/ListItemMessageSkeleton"; type FooterProps = { category: MessageListCategory; @@ -17,6 +17,6 @@ export const Footer = ({ category }: FooterProps) => { return null; } return ( - + ); }; diff --git a/ts/features/messages/components/Home/MessageList.tsx b/ts/features/messages/components/Home/MessageList.tsx index 4230643c2c5..7f7f012a8ab 100644 --- a/ts/features/messages/components/Home/MessageList.tsx +++ b/ts/features/messages/components/Home/MessageList.tsx @@ -20,9 +20,9 @@ import { import { UIMessage } from "../../types"; import { MessageListCategory } from "../../types/messageListCategory"; import { - MessageListItemSkeleton, + ListItemMessageSkeleton, SkeletonHeight -} from "./DS/MessageListItemSkeleton"; +} from "./DS/ListItemMessageSkeleton"; import { EmptyList } from "./EmptyList"; import { Footer } from "./Footer"; import { @@ -32,7 +32,7 @@ import { LayoutInfo, trackMessageListEndReachedIfAllowed } from "./homeUtils"; -import { WrappedMessageListItem } from "./WrappedMessageListItem"; +import { WrappedListItemMessage } from "./WrappedListItemMessage"; const styles = StyleSheet.create({ contentContainer: { @@ -127,13 +127,13 @@ export const MessageList = React.forwardRef( renderItem={({ index, item }) => { if (typeof item === "number") { return ( - ); } else { return ( - { +}: WrappedListItemMessage) => { const dispatch = useIODispatch(); const navigation = useIONavigation(); const store = useIOStore(); @@ -59,7 +59,7 @@ export const WrappedMessageListItem = ({ ); const messageCategoryTag = message.category.tag; - const doubleAvatar = messageCategoryTag === PaymentTagEnum.PAYMENT; + const avatarDouble = messageCategoryTag === PaymentTagEnum.PAYMENT; const serviceLogoUriSources = useMemo( () => logoForService(serviceId, organizationFiscalCode), [serviceId, organizationFiscalCode] @@ -73,19 +73,22 @@ export const WrappedMessageListItem = ({ message.createdAt, I18n.t("messages.yesterday") ); + const isRead = message.isRead; - const badgeText = - messageCategoryTag === SENDTagEnum.PN - ? I18n.t("features.pn.details.badge.legalValue") - : isPaymentMessageWithPaidNotice - ? I18n.t("messages.badge.paid") - : undefined; - const badgeVariant = + + const tag: ListItemMessage["tag"] = messageCategoryTag === SENDTagEnum.PN - ? "legalMessage" + ? { + variant: "legalMessage", + text: I18n.t("features.pn.details.badge.legalValue") + } : isPaymentMessageWithPaidNotice - ? "success" + ? { + variant: "success", + text: I18n.t("messages.badge.paid") + } : undefined; + const accessibilityLabel = useMemo( () => accessibilityLabelForMessageItem(message, isSelected), [isSelected, message] @@ -157,11 +160,10 @@ export const WrappedMessageListItem = ({ ]); return ( - source === "ARCHIVE" || isInboxSource(source); -export const isInboxSource = (source: WrappedMessageListItemProps["source"]) => +export const isInboxSource = (source: WrappedListItemMessage["source"]) => source === "INBOX"; -export const isSearchSource = (source: WrappedMessageListItemProps["source"]) => +export const isSearchSource = (source: WrappedListItemMessage["source"]) => source === "SEARCH"; diff --git a/ts/features/messages/components/Home/__tests__/WrappedMessageListItem.test.tsx b/ts/features/messages/components/Home/__tests__/WrappedListItemMessage.test.tsx similarity index 97% rename from ts/features/messages/components/Home/__tests__/WrappedMessageListItem.test.tsx rename to ts/features/messages/components/Home/__tests__/WrappedListItemMessage.test.tsx index 311976303c7..e157e55e211 100644 --- a/ts/features/messages/components/Home/__tests__/WrappedMessageListItem.test.tsx +++ b/ts/features/messages/components/Home/__tests__/WrappedListItemMessage.test.tsx @@ -10,7 +10,7 @@ import { UIMessage } from "../../../types"; import { MESSAGES_ROUTES } from "../../../navigation/routes"; import { TagEnum as SENDTagEnum } from "../../../../../../definitions/backend/MessageCategoryPN"; import { TagEnum as PaymentTagEnum } from "../../../../../../definitions/backend/MessageCategoryPayment"; -import { WrappedMessageListItem } from "../WrappedMessageListItem"; +import { WrappedListItemMessage } from "../WrappedListItemMessage"; import { TagEnum } from "../../../../../../definitions/backend/MessageCategoryBase"; import { GlobalState } from "../../../../../store/reducers/types"; import { @@ -34,7 +34,7 @@ jest.mock("react-redux", () => ({ useDispatch: () => mockDispatch })); -describe("WrappedMessageListItem", () => { +describe("WrappedListItemMessage", () => { beforeEach(() => { jest.resetAllMocks(); jest.clearAllMocks(); @@ -172,7 +172,7 @@ const renderComponent = ( } as GlobalState; const store = createStore(appReducer, stateWithPayment as any); return renderScreenWithNavigationStoreContext( - () => , + () => , MESSAGES_ROUTES.MESSAGES_HOME, {}, store diff --git a/ts/features/messages/components/Home/__tests__/__snapshots__/WrappedMessageListItem.test.tsx.snap b/ts/features/messages/components/Home/__tests__/__snapshots__/WrappedListItemMessage.test.tsx.snap similarity index 87% rename from ts/features/messages/components/Home/__tests__/__snapshots__/WrappedMessageListItem.test.tsx.snap rename to ts/features/messages/components/Home/__tests__/__snapshots__/WrappedListItemMessage.test.tsx.snap index 71da455b253..8ce8be171c4 100644 --- a/ts/features/messages/components/Home/__tests__/__snapshots__/WrappedMessageListItem.test.tsx.snap +++ b/ts/features/messages/components/Home/__tests__/__snapshots__/WrappedListItemMessage.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`WrappedMessageListItem should match snapshot, from SEND, read message 1`] = ` +exports[`WrappedListItemMessage should match snapshot, from SEND, read message 1`] = ` @@ -720,229 +716,231 @@ exports[`WrappedMessageListItem should match snapshot, from SEND, read message 1 - - - - - + > + + + + + + + Legal value + - - Legal value - - - - - - - - - - + > + + + + @@ -964,7 +962,7 @@ exports[`WrappedMessageListItem should match snapshot, from SEND, read message 1 `; -exports[`WrappedMessageListItem should match snapshot, from SEND, unread message 1`] = ` +exports[`WrappedListItemMessage should match snapshot, from SEND, unread message 1`] = ` @@ -1739,229 +1733,231 @@ exports[`WrappedMessageListItem should match snapshot, from SEND, unread message - - - - - + > + + + + + + + Legal value + - - Legal value - - - - - - - - - - + > + + + + @@ -1983,7 +1979,7 @@ exports[`WrappedMessageListItem should match snapshot, from SEND, unread message `; -exports[`WrappedMessageListItem should match snapshot, not from SEND, contains paid payment, read message 1`] = ` +exports[`WrappedListItemMessage should match snapshot, not from SEND, contains paid payment, read message 1`] = ` @@ -2785,175 +2777,184 @@ exports[`WrappedMessageListItem should match snapshot, not from SEND, contains p dynamicTypeRamp="footnote" maxFontSizeMultiplier={1.25} style={ - [ - {}, - { - "color": "#555C70", - "fontFamily": "Titillium Sans Pro", - "fontSize": 14, - "fontStyle": "normal", - "fontWeight": "600", - "lineHeight": 21, - }, - ] - } - > - Service name · - - - Message title - - - - - - - - - - - - - + > + Service name · + + Message title + + + + + + - Paid - + + + + + + + + + + Paid + + @@ -2974,7 +2975,7 @@ exports[`WrappedMessageListItem should match snapshot, not from SEND, contains p `; -exports[`WrappedMessageListItem should match snapshot, not from SEND, contains paid payment, unread message 1`] = ` +exports[`WrappedListItemMessage should match snapshot, not from SEND, contains paid payment, unread message 1`] = ` @@ -3871,135 +3868,144 @@ exports[`WrappedMessageListItem should match snapshot, not from SEND, contains p - - - - - - - - + + + + + - Paid - + "width": 6, + } + } + /> + + Paid + + @@ -4020,7 +4026,7 @@ exports[`WrappedMessageListItem should match snapshot, not from SEND, contains p `; -exports[`WrappedMessageListItem should match snapshot, not from SEND, contains unpaid payment, read message 1`] = ` +exports[`WrappedListItemMessage should match snapshot, not from SEND, contains unpaid payment, read message 1`] = ` @@ -4877,7 +4879,7 @@ exports[`WrappedMessageListItem should match snapshot, not from SEND, contains u `; -exports[`WrappedMessageListItem should match snapshot, not from SEND, contains unpaid payment, unread message 1`] = ` +exports[`WrappedListItemMessage should match snapshot, not from SEND, contains unpaid payment, unread message 1`] = ` @@ -5789,7 +5787,7 @@ exports[`WrappedMessageListItem should match snapshot, not from SEND, contains u `; -exports[`WrappedMessageListItem should match snapshot, not from SEND, not a payment, read message 1`] = ` +exports[`WrappedListItemMessage should match snapshot, not from SEND, not a payment, read message 1`] = ` @@ -6524,7 +6518,7 @@ exports[`WrappedMessageListItem should match snapshot, not from SEND, not a paym `; -exports[`WrappedMessageListItem should match snapshot, not from SEND, not a payment, unread message 1`] = ` +exports[`WrappedListItemMessage should match snapshot, not from SEND, not a payment, unread message 1`] = ` diff --git a/ts/features/messages/components/Home/homeUtils.ts b/ts/features/messages/components/Home/homeUtils.ts index 95f3d90160b..f3b4539925a 100644 --- a/ts/features/messages/components/Home/homeUtils.ts +++ b/ts/features/messages/components/Home/homeUtils.ts @@ -31,8 +31,11 @@ import { TagEnum } from "../../../../../definitions/backend/MessageCategoryPN"; import NavigationService from "../../../../navigation/NavigationService"; import { trackMessageListEndReached, trackMessagesPage } from "../../analytics"; import { MESSAGES_ROUTES } from "../../navigation/routes"; -import { EnhancedHeight, StandardHeight } from "./DS/MessageListItem"; -import { SkeletonHeight } from "./DS/MessageListItemSkeleton"; +import { + ListItemMessageEnhancedHeight, + ListItemMessageStandardHeight +} from "./DS/ListItemMessage"; +import { SkeletonHeight } from "./DS/ListItemMessageSkeleton"; export type LayoutInfo = { index: number; @@ -247,7 +250,9 @@ export const generateMessageListLayoutInfo = ( isPaymentMessageWithPaidNoticeSelector(state, message.category); const itemLayoutInfo: LayoutInfo = { index: i, - length: messageHasBadge ? EnhancedHeight : StandardHeight, + length: messageHasBadge + ? ListItemMessageEnhancedHeight + : ListItemMessageStandardHeight, offset: i > 0 ? messageListLayoutInfo[i - 1].offset + diff --git a/ts/features/messages/components/MessageDetail/OrganizationHeader.tsx b/ts/features/messages/components/MessageDetail/OrganizationHeader.tsx index 44d59ab6921..acb02120b2b 100644 --- a/ts/features/messages/components/MessageDetail/OrganizationHeader.tsx +++ b/ts/features/messages/components/MessageDetail/OrganizationHeader.tsx @@ -14,7 +14,7 @@ import { useIOSelector } from "../../../../store/hooks"; import { SERVICES_ROUTES } from "../../../services/common/navigation/routes"; import { messagePaymentDataSelector } from "../../store/reducers/detailsById"; import { UIMessageId } from "../../types"; -import { DoubleAvatar } from "../Home/DS/DoubleAvatar"; +import { AvatarDouble } from "../Home/DS/AvatarDouble"; export type OrganizationHeaderProps = { messageId: UIMessageId; @@ -77,7 +77,7 @@ export const OrganizationHeader = ({ {paymentData ? ( - + ) : ( )} diff --git a/ts/features/messages/screens/MessagesSearchScreen.tsx b/ts/features/messages/screens/MessagesSearchScreen.tsx index 1830f7e5da6..17c1e152fa9 100644 --- a/ts/features/messages/screens/MessagesSearchScreen.tsx +++ b/ts/features/messages/screens/MessagesSearchScreen.tsx @@ -27,7 +27,7 @@ import { EmptyList } from "../components/Search/EmptyList"; import { useIONavigation } from "../../../navigation/params/AppParamsList"; import { useIOStore } from "../../../store/hooks"; import { UIMessage } from "../types"; -import { WrappedMessageListItem } from "../components/Home/WrappedMessageListItem"; +import { WrappedListItemMessage } from "../components/Home/WrappedListItemMessage"; import { trackMessageSearchClosing, trackMessageSearchPage @@ -58,7 +58,7 @@ export const MessagesSearchScreen = () => { const renderItemCallback = useCallback( (itemInfo: ListRenderItemInfo) => ( - { + }: ListItemSearchInstitution) => { const { isExperimental } = useIOExperimentalDesign(); const theme = useIOTheme(); From fbe9e87eb00f576a09867991353b1b82a5f5fea3 Mon Sep 17 00:00:00 2001 From: RiccardoMolinari95 <98079356+RiccardoMolinari95@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:42:51 +0100 Subject: [PATCH 4/9] feat(IT Wallet): [SIW-1824] Show alert if wallet instance is revoked (#6547) ## Short description This PR adds functionality to display an alert in the wallet section when the Wallet Instance has been revoked. The alerts are displayed based on the revocation reason. ## List of changes proposed in this pull request - add `itwUpdateWalletInstanceStatus` in `getStatusOrResetWalletInstance` to update the wallet instance status - update the global state of Wallet Instance including revocation details - add `itwWalletInstanceStatusSelector` to retrieve WI status - add `useItwWalletInstanceRevocationAlert` to display `Alert` in based on the revocation reason ## How to test Simulate or revoked via Web the wallet instance revocation and navigate to wallet section CERTIFICATE_REVOKED_BY_ISSUER https://github.com/user-attachments/assets/9aa867fb-f690-4da4-8e19-a1e53298e6de REVOKED_BY_USER https://github.com/user-attachments/assets/24899e8b-0110-4228-8edc-8f2b6da3e7e3 --- .env.local | 4 +- .env.production | 4 +- locales/en/index.yml | 14 +++ locales/it/index.yml | 14 +++ ts/config.ts | 5 - .../__snapshots__/index.test.ts.snap | 1 + .../__snapshots__/index.test.ts.snap | 1 + .../itwallet/common/utils/itwTypesUtils.ts | 19 ++- .../checkWalletInstanceStateSaga.test.ts | 2 +- .../saga/checkWalletInstanceStateSaga.ts | 4 + .../itwallet/machine/credential/actions.ts | 2 +- ts/features/itwallet/machine/eid/actions.ts | 2 +- .../itwallet/trustmark/machine/actions.ts | 2 +- .../useItwWalletInstanceRevocationAlert.ts | 101 ++++++++++++++++ .../walletInstance/store/actions/index.ts | 14 ++- .../walletInstance/store/reducers/index.ts | 35 +++--- .../walletInstance/store/selectors/index.ts | 23 ++++ .../components/WalletCardsContainer.tsx | 3 + .../__tests__/WalletCardsContainer.test.tsx | 110 ++++++++++++++++++ 19 files changed, 322 insertions(+), 38 deletions(-) create mode 100644 ts/features/itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert.ts create mode 100644 ts/features/itwallet/walletInstance/store/selectors/index.ts diff --git a/.env.local b/.env.local index 15b6ff0d3a8..ead00523583 100644 --- a/.env.local +++ b/.env.local @@ -97,6 +97,4 @@ ITW_BYPASS_IDENTITY_MATCH=YES # Use the test environment for the IDP hint for both CIE and SPID ITW_IDP_HINT_TEST=YES # IPZS Privacy Policy URL -ITW_IPZS_PRIVACY_URL='https://io.italia.it/informativa-ipzs' -# ITW Documents on IO URL -ITW_DOCUMENTS_ON_IO_URL='https://io.italia.it/documenti-su-io' +ITW_IPZS_PRIVACY_URL='https://io.italia.it/informativa-ipzs' \ No newline at end of file diff --git a/.env.production b/.env.production index 0d09f9f56db..6d01eebd819 100644 --- a/.env.production +++ b/.env.production @@ -97,6 +97,4 @@ ITW_BYPASS_IDENTITY_MATCH=NO # Use the test environment for the IDP hint for both CIE and SPID ITW_IDP_HINT_TEST=NO # IPZS Privacy Policy URL -ITW_IPZS_PRIVACY_URL='https://io.italia.it/informativa-ipzs' -# ITW Documents on IO URL -ITW_DOCUMENTS_ON_IO_URL='https://io.italia.it/documenti-su-io' +ITW_IPZS_PRIVACY_URL='https://io.italia.it/informativa-ipzs' \ No newline at end of file diff --git a/locales/en/index.yml b/locales/en/index.yml index 8c2b33fac22..a10530a05a0 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3555,6 +3555,20 @@ features: title: Dicci cosa ne pensi content: Raccontaci la tua esperienza con la funzionalità Documenti su IO. action: Inizia + walletInstanceRevoked: + alert: + cta: Scopri di più + closeButton: Chiudi + closeButtonAlt: Ho capito + revokedByWalletProvider: + title: Documenti su IO è stata disattivata + content: Per verificare i requisiti richiesti per continuare a usare la funzionalità sul tuo dispositivo, premi "Scopri di più". + newWalletInstanceCreated: + title: Documenti su IO è stata disattivata su questo dispositivo + content: Puoi usare i tuoi documenti su IO su un solo dispositivo alla volta per ragioni di sicurezza. + revokedByUser: + title: Hai disattivato Documenti su IO + content: Se cambi idea, potrai riattivare Documenti su IO in futuro. support: ticketList: noTicket: diff --git a/locales/it/index.yml b/locales/it/index.yml index 640a6ab0a72..3a5cbe020ce 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3555,6 +3555,20 @@ features: title: Dicci cosa ne pensi content: Raccontaci la tua esperienza con la funzionalità Documenti su IO. action: Inizia + walletInstanceRevoked: + alert: + cta: Scopri di più + closeButton: Chiudi + closeButtonAlt: Ho capito + revokedByWalletProvider: + title: Documenti su IO è stata disattivata + content: Per verificare i requisiti richiesti per continuare a usare la funzionalità sul tuo dispositivo, premi "Scopri di più". + newWalletInstanceCreated: + title: Documenti su IO è stata disattivata su questo dispositivo + content: Puoi usare i tuoi documenti su IO su un solo dispositivo alla volta per ragioni di sicurezza. + revokedByUser: + title: Hai disattivato Documenti su IO + content: Se cambi idea, potrai riattivare Documenti su IO in futuro. support: ticketList: noTicket: diff --git a/ts/config.ts b/ts/config.ts index fa847bbb3ad..505771f19eb 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -252,8 +252,3 @@ export const itwIpzsPrivacyUrl: string = pipe( t.string.decode, E.getOrElse(() => "https://io.italia.it/informativa-ipzs") ); -export const itwDocumentsOnIOUrl: string = pipe( - Config.ITW_DOCUMENTS_ON_IO_URL, - t.string.decode, - E.getOrElse(() => "https://io.italia.it/documenti-su-io") -); diff --git a/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap b/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap index 527cdcd64ce..81dd7b4bcb0 100644 --- a/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap +++ b/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap @@ -132,6 +132,7 @@ exports[`featuresPersistor should match snapshot 1`] = ` }, "walletInstance": { "attestation": undefined, + "status": undefined, }, }, "landingBanners": { diff --git a/ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap b/ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap index 7fb4ec75a32..e6f11fb8d17 100644 --- a/ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap +++ b/ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap @@ -19,6 +19,7 @@ exports[`itWalletReducer should match snapshot [if this test fails, remember to }, "walletInstance": { "attestation": undefined, + "status": undefined, }, } `; diff --git a/ts/features/itwallet/common/utils/itwTypesUtils.ts b/ts/features/itwallet/common/utils/itwTypesUtils.ts index fe04304f663..3fab058e345 100644 --- a/ts/features/itwallet/common/utils/itwTypesUtils.ts +++ b/ts/features/itwallet/common/utils/itwTypesUtils.ts @@ -1,4 +1,8 @@ -import { Credential, Trust } from "@pagopa/io-react-native-wallet"; +import { + Credential, + Trust, + WalletInstance +} from "@pagopa/io-react-native-wallet"; /** * Alias type for the return type of the start issuance flow operation. @@ -43,6 +47,19 @@ export type ParsedStatusAttestation = Awaited< ReturnType >["parsedStatusAttestation"]["payload"]; +/** + * Alias for the WalletInstanceStatus type + */ +export type WalletInstanceStatus = Awaited< + ReturnType +>; + +/** + * Alias for the WalletInstanceRevocationReason type + */ +export type WalletInstanceRevocationReason = + WalletInstanceStatus["revocation_reason"]; + export type StoredStatusAttestation = | { credentialStatus: "valid"; diff --git a/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts b/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts index fdf253d364c..574090dea19 100644 --- a/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts +++ b/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts @@ -12,9 +12,9 @@ import { getWalletInstanceStatus } from "../../../common/utils/itwAttestationUti import { StoredCredential } from "../../../common/utils/itwTypesUtils"; import { sessionTokenSelector } from "../../../../../store/reducers/authentication"; import { handleWalletInstanceResetSaga } from "../handleWalletInstanceResetSaga"; -import { itwIsWalletInstanceAttestationValidSelector } from "../../../walletInstance/store/reducers"; import { ensureIntegrityServiceIsReady } from "../../../common/utils/itwIntegrityUtils"; import { itwIntegrityServiceReadySelector } from "../../../issuance/store/selectors"; +import { itwIsWalletInstanceAttestationValidSelector } from "../../../walletInstance/store/selectors"; jest.mock("@pagopa/io-react-native-crypto", () => ({ deleteKey: jest.fn diff --git a/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts b/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts index 779a11a929e..07d6dc12719 100644 --- a/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts +++ b/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts @@ -8,6 +8,7 @@ import { ensureIntegrityServiceIsReady } from "../../common/utils/itwIntegrityUt import { itwIntegrityKeyTagSelector } from "../../issuance/store/selectors"; import { itwLifecycleIsOperationalOrValid } from "../store/selectors"; import { itwIntegritySetServiceIsReady } from "../../issuance/store/actions"; +import { itwUpdateWalletInstanceStatus } from "../../walletInstance/store/actions"; import { handleWalletInstanceResetSaga } from "./handleWalletInstanceResetSaga"; export function* getStatusOrResetWalletInstance(integrityKeyTag: string) { @@ -23,6 +24,9 @@ export function* getStatusOrResetWalletInstance(integrityKeyTag: string) { if (walletInstanceStatus.is_revoked) { yield* call(handleWalletInstanceResetSaga); } + + // Update wallet instance status + yield* put(itwUpdateWalletInstanceStatus(walletInstanceStatus)); } /** diff --git a/ts/features/itwallet/machine/credential/actions.ts b/ts/features/itwallet/machine/credential/actions.ts index f5f1eaf9990..ee43befc3b9 100644 --- a/ts/features/itwallet/machine/credential/actions.ts +++ b/ts/features/itwallet/machine/credential/actions.ts @@ -19,7 +19,7 @@ import { import { itwCredentialsStore } from "../../credentials/store/actions"; import { ITW_ROUTES } from "../../navigation/routes"; import { itwWalletInstanceAttestationStore } from "../../walletInstance/store/actions"; -import { itwWalletInstanceAttestationSelector } from "../../walletInstance/store/reducers"; +import { itwWalletInstanceAttestationSelector } from "../../walletInstance/store/selectors"; import { Context } from "./context"; import { CredentialIssuanceEvents } from "./events"; diff --git a/ts/features/itwallet/machine/eid/actions.ts b/ts/features/itwallet/machine/eid/actions.ts index d7581cc2b7a..9b84d7a1e23 100644 --- a/ts/features/itwallet/machine/eid/actions.ts +++ b/ts/features/itwallet/machine/eid/actions.ts @@ -22,8 +22,8 @@ import { trackSaveCredentialSuccess, updateITWStatusAndIDProperties } from "../../analytics"; -import { itwWalletInstanceAttestationSelector } from "../../walletInstance/store/reducers"; import { itwIntegrityKeyTagSelector } from "../../issuance/store/selectors"; +import { itwWalletInstanceAttestationSelector } from "../../walletInstance/store/selectors"; import { Context } from "./context"; import { EidIssuanceEvents } from "./events"; diff --git a/ts/features/itwallet/trustmark/machine/actions.ts b/ts/features/itwallet/trustmark/machine/actions.ts index 929d2d32104..145a4ba00fd 100644 --- a/ts/features/itwallet/trustmark/machine/actions.ts +++ b/ts/features/itwallet/trustmark/machine/actions.ts @@ -4,7 +4,7 @@ import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { checkCurrentSession } from "../../../../store/actions/authentication"; import { useIOStore } from "../../../../store/hooks"; import { itwCredentialByTypeSelector } from "../../credentials/store/selectors"; -import { itwWalletInstanceAttestationSelector } from "../../walletInstance/store/reducers"; +import { itwWalletInstanceAttestationSelector } from "../../walletInstance/store/selectors"; import { Context } from "./context"; export const createItwTrustmarkActionsImplementation = ( diff --git a/ts/features/itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert.ts b/ts/features/itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert.ts new file mode 100644 index 00000000000..15698a1baf2 --- /dev/null +++ b/ts/features/itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert.ts @@ -0,0 +1,101 @@ +import { Alert } from "react-native"; +import { useCallback } from "react"; +import I18n from "../../../../i18n"; +import { WalletInstanceRevocationReason } from "../../common/utils/itwTypesUtils"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { itwWalletInstanceStatusSelector } from "../store/selectors"; +import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; +import { itwUpdateWalletInstanceStatus } from "../store/actions"; +import { openWebUrl } from "../../../../utils/url"; + +const closeButtonText = I18n.t( + "features.itWallet.walletInstanceRevoked.alert.closeButton" +); +const alertCtaText = I18n.t( + "features.itWallet.walletInstanceRevoked.alert.cta" +); + +const itwMinIntegrityReqUrl = "https://io.italia.it/documenti-su-io/faq/#n1_12"; +const itwDocsOnIOMultipleDevicesUrl = + "https://io.italia.it/documenti-su-io/faq/#n1_14"; + +/** + * Hook to monitor wallet instance status and display alerts if revoked. + */ +export const useItwWalletInstanceRevocationAlert = () => { + const walletInstanceStatus = useIOSelector(itwWalletInstanceStatusSelector); + const dispatch = useIODispatch(); + + useOnFirstRender( + useCallback(() => { + if (walletInstanceStatus?.is_revoked) { + showWalletRevocationAlert(walletInstanceStatus.revocation_reason); + dispatch(itwUpdateWalletInstanceStatus(undefined)); + } + }, [walletInstanceStatus, dispatch]) + ); +}; + +/** + * Displays an alert based on the revocation reason. + */ +const showWalletRevocationAlert = ( + revocationReason?: WalletInstanceRevocationReason +) => { + switch (revocationReason) { + case "CERTIFICATE_REVOKED_BY_ISSUER": + Alert.alert( + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.revokedByWalletProvider.title" + ), + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.revokedByWalletProvider.content" + ), + [ + { text: closeButtonText }, + { + text: alertCtaText, + onPress: () => openWebUrl(itwMinIntegrityReqUrl) + } + ] + ); + break; + + case "NEW_WALLET_INSTANCE_CREATED": + Alert.alert( + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.newWalletInstanceCreated.title" + ), + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.newWalletInstanceCreated.content" + ), + [ + { text: closeButtonText }, + { + text: alertCtaText, + onPress: () => openWebUrl(itwDocsOnIOMultipleDevicesUrl) + } + ] + ); + break; + case "REVOKED_BY_USER": + Alert.alert( + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.revokedByUser.title" + ), + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.revokedByUser.content" + ), + [ + { + text: I18n.t( + "features.itWallet.walletInstanceRevoked.alert.closeButtonAlt" + ) + } + ] + ); + break; + default: + break; + } +}; diff --git a/ts/features/itwallet/walletInstance/store/actions/index.ts b/ts/features/itwallet/walletInstance/store/actions/index.ts index 3d0c05b05c2..cbf6db80535 100644 --- a/ts/features/itwallet/walletInstance/store/actions/index.ts +++ b/ts/features/itwallet/walletInstance/store/actions/index.ts @@ -1,4 +1,5 @@ import { ActionType, createStandardAction } from "typesafe-actions"; +import { WalletInstanceStatus } from "../../../common/utils/itwTypesUtils"; /** * This action stores the Wallet Instance Attestation @@ -7,6 +8,13 @@ export const itwWalletInstanceAttestationStore = createStandardAction( "ITW_WALLET_INSTANCE_ATTESTATION_STORE" )(); -export type ItwWalletInstanceActions = ActionType< - typeof itwWalletInstanceAttestationStore ->; +/** + * This action update the Wallet Instance Status + */ +export const itwUpdateWalletInstanceStatus = createStandardAction( + "ITW_WALLET_INSTANCE_STATUS_UPDATE" +)(); + +export type ItwWalletInstanceActions = + | ActionType + | ActionType; diff --git a/ts/features/itwallet/walletInstance/store/reducers/index.ts b/ts/features/itwallet/walletInstance/store/reducers/index.ts index 2494833ff35..778d134d4fb 100644 --- a/ts/features/itwallet/walletInstance/store/reducers/index.ts +++ b/ts/features/itwallet/walletInstance/store/reducers/index.ts @@ -1,21 +1,22 @@ -import * as O from "fp-ts/lib/Option"; -import { flow } from "fp-ts/lib/function"; import { PersistConfig, persistReducer } from "redux-persist"; -import { createSelector } from "reselect"; import { getType } from "typesafe-actions"; import { Action } from "../../../../../store/actions/types"; -import { GlobalState } from "../../../../../store/reducers/types"; import itwCreateSecureStorage from "../../../common/store/storages/itwSecureStorage"; -import { isWalletInstanceAttestationValid } from "../../../common/utils/itwAttestationUtils"; import { itwLifecycleStoresReset } from "../../../lifecycle/store/actions"; -import { itwWalletInstanceAttestationStore } from "../actions"; +import { + itwWalletInstanceAttestationStore, + itwUpdateWalletInstanceStatus +} from "../actions"; +import { WalletInstanceStatus } from "../../../common/utils/itwTypesUtils"; export type ItwWalletInstanceState = { attestation: string | undefined; + status: WalletInstanceStatus | undefined; }; export const itwWalletInstanceInitialState: ItwWalletInstanceState = { - attestation: undefined + attestation: undefined, + status: undefined }; const CURRENT_REDUX_ITW_WALLET_INSTANCE_STORE_VERSION = -1; @@ -27,10 +28,18 @@ const reducer = ( switch (action.type) { case getType(itwWalletInstanceAttestationStore): { return { + status: undefined, attestation: action.payload }; } + case getType(itwUpdateWalletInstanceStatus): { + return { + ...state, + status: action.payload + }; + } + case getType(itwLifecycleStoresReset): return { ...itwWalletInstanceInitialState }; @@ -50,16 +59,4 @@ const persistedReducer = persistReducer( reducer ); -export const itwWalletInstanceAttestationSelector = (state: GlobalState) => - state.features.itWallet.walletInstance.attestation; - -export const itwIsWalletInstanceAttestationValidSelector = createSelector( - itwWalletInstanceAttestationSelector, - flow( - O.fromNullable, - O.map(isWalletInstanceAttestationValid), - O.getOrElse(() => false) - ) -); - export default persistedReducer; diff --git a/ts/features/itwallet/walletInstance/store/selectors/index.ts b/ts/features/itwallet/walletInstance/store/selectors/index.ts new file mode 100644 index 00000000000..85a98084d61 --- /dev/null +++ b/ts/features/itwallet/walletInstance/store/selectors/index.ts @@ -0,0 +1,23 @@ +import * as O from "fp-ts/lib/Option"; +import { flow } from "fp-ts/lib/function"; +import { createSelector } from "reselect"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { isWalletInstanceAttestationValid } from "../../../common/utils/itwAttestationUtils"; + +/* Selector to get the wallet instance attestation */ +export const itwWalletInstanceAttestationSelector = (state: GlobalState) => + state.features.itWallet.walletInstance.attestation; + +/* Selector to check if the attestation is valid */ +export const itwIsWalletInstanceAttestationValidSelector = createSelector( + itwWalletInstanceAttestationSelector, + flow( + O.fromNullable, + O.map(isWalletInstanceAttestationValid), + O.getOrElse(() => false) + ) +); + +/* Selector to get the wallet instance status */ +export const itwWalletInstanceStatusSelector = (state: GlobalState) => + state.features.itWallet.walletInstance.status; diff --git a/ts/features/wallet/components/WalletCardsContainer.tsx b/ts/features/wallet/components/WalletCardsContainer.tsx index 394dc54ee19..9d08e1c2a8b 100644 --- a/ts/features/wallet/components/WalletCardsContainer.tsx +++ b/ts/features/wallet/components/WalletCardsContainer.tsx @@ -28,6 +28,7 @@ import { shouldRenderWalletEmptyStateSelector } from "../store/selectors"; import { WalletCardCategoryFilter } from "../types"; +import { useItwWalletInstanceRevocationAlert } from "../../itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert"; import { WalletCardsCategoryContainer } from "./WalletCardsCategoryContainer"; import { WalletCardsCategoryRetryErrorBanner } from "./WalletCardsCategoryRetryErrorBanner"; import { WalletCardSkeleton } from "./WalletCardSkeleton"; @@ -48,6 +49,8 @@ const WalletCardsContainer = () => { shouldRenderWalletEmptyStateSelector ); + useItwWalletInstanceRevocationAlert(); + // Loading state is only displayed if there is the initial loading and there are no cards or // placeholders in the wallet const shouldRenderLoadingState = isLoading && isWalletEmpty; diff --git a/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx b/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx index 45aabf4fb3b..c364c9f9a4f 100644 --- a/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx +++ b/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx @@ -2,6 +2,7 @@ import * as O from "fp-ts/lib/Option"; import _ from "lodash"; import * as React from "react"; import configureMockStore from "redux-mock-store"; +import { Alert } from "react-native"; import ROUTES from "../../../../navigation/routes"; import { applicationChangeState } from "../../../../store/actions/application"; import { appReducer } from "../../../../store/reducers"; @@ -16,6 +17,7 @@ import { import { ItwJwtCredentialStatus } from "../../../itwallet/common/utils/itwTypesUtils"; import * as itwCredentialsSelectors from "../../../itwallet/credentials/store/selectors"; import * as itwLifecycleSelectors from "../../../itwallet/lifecycle/store/selectors"; +import * as itwWalletInstanceSelectors from "../../../itwallet/walletInstance/store/selectors"; import { WalletCardsState } from "../../store/reducers/cards"; import * as walletSelectors from "../../store/selectors"; import { WalletCard } from "../../types"; @@ -24,7 +26,9 @@ import { OtherWalletCardsContainer, WalletCardsContainer } from "../WalletCardsContainer"; +import I18n from "../../../../i18n"; +jest.spyOn(Alert, "alert"); jest.mock("react-native-reanimated", () => ({ ...require("react-native-reanimated/mock"), useReducedMotion: jest.fn, @@ -357,6 +361,112 @@ describe("OtherWalletCardsContainer", () => { expect(queryByTestId(`walletCardTestID_cgn_cgn_3`)).not.toBeNull(); expect(queryByTestId(`walletCardTestID_itw_placeholder_4`)).not.toBeNull(); }); + + it("should not show alert if not revoked", () => { + jest + .spyOn(itwWalletInstanceSelectors, "itwWalletInstanceStatusSelector") + .mockImplementation(() => ({ + id: "39cc62ab-1df0-4a9d-974d-4c58173a1750", + is_revoked: false, + revocation_reason: undefined + })); + + renderComponent(WalletCardsContainer); + + expect(Alert.alert).not.toHaveBeenCalled(); + }); + + it("should show alert for NEW_WALLET_INSTANCE_CREATED", () => { + jest + .spyOn(itwWalletInstanceSelectors, "itwWalletInstanceStatusSelector") + .mockImplementation(() => ({ + id: "39cc62ab-1df0-4a9d-974d-4c58173a1750", + is_revoked: true, + revocation_reason: "NEW_WALLET_INSTANCE_CREATED" + })); + + renderComponent(WalletCardsContainer); + + expect(Alert.alert).toHaveBeenCalledWith( + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.newWalletInstanceCreated.title" + ), + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.newWalletInstanceCreated.content" + ), + [ + { + text: I18n.t( + "features.itWallet.walletInstanceRevoked.alert.closeButton" + ) + }, + { + text: I18n.t("features.itWallet.walletInstanceRevoked.alert.cta"), + onPress: expect.any(Function) + } + ] + ); + }); + + it("should show alert for CERTIFICATE_REVOKED_BY_ISSUER", () => { + jest + .spyOn(itwWalletInstanceSelectors, "itwWalletInstanceStatusSelector") + .mockImplementation(() => ({ + id: "39cc62ab-1df0-4a9d-974d-4c58173a1750", + is_revoked: true, + revocation_reason: "CERTIFICATE_REVOKED_BY_ISSUER" + })); + + renderComponent(WalletCardsContainer); + + expect(Alert.alert).toHaveBeenCalledWith( + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.revokedByWalletProvider.title" + ), + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.revokedByWalletProvider.content" + ), + [ + { + text: I18n.t( + "features.itWallet.walletInstanceRevoked.alert.closeButton" + ) + }, + { + text: I18n.t("features.itWallet.walletInstanceRevoked.alert.cta"), + onPress: expect.any(Function) + } + ] + ); + }); + + it("should show alert for REVOKED_BY_USER", () => { + jest + .spyOn(itwWalletInstanceSelectors, "itwWalletInstanceStatusSelector") + .mockImplementation(() => ({ + id: "39cc62ab-1df0-4a9d-974d-4c58173a1750", + is_revoked: true, + revocation_reason: "REVOKED_BY_USER" + })); + + renderComponent(WalletCardsContainer); + + expect(Alert.alert).toHaveBeenCalledWith( + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.revokedByUser.title" + ), + I18n.t( + "features.itWallet.walletInstanceRevoked.alert.revokedByUser.content" + ), + [ + { + text: I18n.t( + "features.itWallet.walletInstanceRevoked.alert.closeButtonAlt" + ) + } + ] + ); + }); }); const renderComponent = (component: React.ComponentType) => { From a835a7017290d43cf1b6edf0a96bcb84cbeb782b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 19 Dec 2024 22:15:26 +0000 Subject: [PATCH 5/9] chore(release): 2.80.0-rc.6 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ android/app/build.gradle | 4 ++-- ios/ItaliaApp.xcodeproj/project.pbxproj | 4 ++-- ios/ItaliaApp/Info.plist | 2 +- ios/ItaliaAppTests/Info.plist | 2 +- package.json | 2 +- publiccode.yml | 2 +- 7 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b90ff95da7..56f31e639a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ 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.80.0-rc.6](https://github.com/pagopa/io-app/compare/2.80.0-rc.5...2.80.0-rc.6) (2024-12-19) + + +### Features + +* **IT Wallet:** [[SIW-1824](https://pagopa.atlassian.net/browse/SIW-1824)] Show alert if wallet instance is revoked ([#6547](https://github.com/pagopa/io-app/issues/6547)) ([fbe9e87](https://github.com/pagopa/io-app/commit/fbe9e87eb00f576a09867991353b1b82a5f5fea3)) + + +### Bug Fixes + +* [[IOPID-2547](https://pagopa.atlassian.net/browse/IOPID-2547)] Fix unhandled error on `Linking.openUrl` ([#6554](https://github.com/pagopa/io-app/issues/6554)) ([1ddf587](https://github.com/pagopa/io-app/commit/1ddf587f5c63c6bf5a236da5a96e31764b86955f)) +* [[IOPLT-814](https://pagopa.atlassian.net/browse/IOPLT-814)] Fixes workflow concatenation after changes due to test improvements ([#6572](https://github.com/pagopa/io-app/issues/6572)) ([d8a8e66](https://github.com/pagopa/io-app/commit/d8a8e660bfc5bf9358e2050763ebeeff5b6b9c4c)) + + +### Chores + +* **Cross:** [[IOAPPX-432](https://pagopa.atlassian.net/browse/IOAPPX-432)] Development Push notifications for Android ([#6416](https://github.com/pagopa/io-app/issues/6416)) ([8cf64a9](https://github.com/pagopa/io-app/commit/8cf64a942a0ac538c0f6b493dccd1d8695bc29d5)) +* **Cross:** [[IOAPPX-448](https://pagopa.atlassian.net/browse/IOAPPX-448)] Add missing components to the Design System section, remove legacy ones + Change `ListItemMessage` component API ([#6541](https://github.com/pagopa/io-app/issues/6541)) ([323996f](https://github.com/pagopa/io-app/commit/323996f8e0207da8a60766ff38042e9a85d9a857)) +* [[IOBP-1076](https://pagopa.atlassian.net/browse/IOBP-1076)] FAQ without CTA with sticky buttons ([#6555](https://github.com/pagopa/io-app/issues/6555)) ([a0d562f](https://github.com/pagopa/io-app/commit/a0d562fc949e40ca7b24b714e42e41f53db56f32)) +* [[IOPLT-798](https://pagopa.atlassian.net/browse/IOPLT-798)] Split test execution using shards ([#6500](https://github.com/pagopa/io-app/issues/6500)) ([7d9df54](https://github.com/pagopa/io-app/commit/7d9df5479cd9784e2679e5a5188d085cb68dbef5)) +* [[IOPLT-813](https://pagopa.atlassian.net/browse/IOPLT-813)] Fix android run script and align Gemfile ([#6571](https://github.com/pagopa/io-app/issues/6571)) ([bbe6980](https://github.com/pagopa/io-app/commit/bbe69800a17a8e0b6b88629da9b58fa7f23f95c4)) +* **IT Wallet:** [[SIW-1793](https://pagopa.atlassian.net/browse/SIW-1793)] Update non-matching identity screen ([#6559](https://github.com/pagopa/io-app/issues/6559)) ([5c04708](https://github.com/pagopa/io-app/commit/5c04708efdf2aa82263c947073eb6c9e489f15f7)) +* [[IOPLT-801](https://pagopa.atlassian.net/browse/IOPLT-801)] Improvements to canary release workflow ([#6468](https://github.com/pagopa/io-app/issues/6468)) ([dc66860](https://github.com/pagopa/io-app/commit/dc668601582efbb029b36c58186e7fb9e098b576)) + ## [2.80.0-rc.5](https://github.com/pagopa/io-app/compare/2.80.0-rc.4...2.80.0-rc.5) (2024-12-18) diff --git a/android/app/build.gradle b/android/app/build.gradle index 320024561f8..8e759e032cf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -119,8 +119,8 @@ android { applicationId "it.pagopa.io.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 100154900 - versionName "2.80.0.5" + versionCode 100154901 + versionName "2.80.0.6" multiDexEnabled true // The resConfigs attribute will remove all not required localized resources while building the application, // including the localized resources from libraries. diff --git a/ios/ItaliaApp.xcodeproj/project.pbxproj b/ios/ItaliaApp.xcodeproj/project.pbxproj index 7e4990bdbc0..70e39b1f27b 100644 --- a/ios/ItaliaApp.xcodeproj/project.pbxproj +++ b/ios/ItaliaApp.xcodeproj/project.pbxproj @@ -798,7 +798,7 @@ CODE_SIGN_ENTITLEMENTS = ItaliaApp/ItaliaApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = M2X5YQ4BJ7; ENABLE_BITCODE = NO; @@ -835,7 +835,7 @@ CODE_SIGN_ENTITLEMENTS = ItaliaApp/ItaliaApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = M2X5YQ4BJ7; ENABLE_BITCODE = NO; diff --git a/ios/ItaliaApp/Info.plist b/ios/ItaliaApp/Info.plist index fffb2ffee8b..fcb81f12f10 100644 --- a/ios/ItaliaApp/Info.plist +++ b/ios/ItaliaApp/Info.plist @@ -36,7 +36,7 @@ CFBundleVersion - 5 + 6 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/ItaliaAppTests/Info.plist b/ios/ItaliaAppTests/Info.plist index 8256df486f4..54df2577125 100644 --- a/ios/ItaliaAppTests/Info.plist +++ b/ios/ItaliaAppTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 5 + 6 \ No newline at end of file diff --git a/package.json b/package.json index 10129d02238..0c590c8a8fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "italia-app", - "version": "2.80.0-rc.5", + "version": "2.80.0-rc.6", "private": true, "scripts": { "start": "react-native start", diff --git a/publiccode.yml b/publiccode.yml index e9c51e4b7cf..9fb9e0d7e57 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -9,7 +9,7 @@ releaseDate: "2024-11-21" url: "https://github.com/pagopa/io-app" applicationSuite: IO landingURL: "https://io.italia.it/" -softwareVersion: 2.80.0-rc.5 +softwareVersion: 2.80.0-rc.6 developmentStatus: beta softwareType: standalone/mobile roadmap: "https://io.italia.it/" From dcde5de8423d95ad7fcc24c059032596b3e85cb2 Mon Sep 17 00:00:00 2001 From: Alessandro Mazzon Date: Fri, 20 Dec 2024 11:20:34 +0100 Subject: [PATCH 6/9] fix(IT Wallet): [SIW-1934] Cleanup the integrityKeyTag if getWalletAttestation throws an error (#6569) ## Short description This PR updates the `WalletInstanceAttestationObtainment` machine step adding a cleanup function if there is an error. ## List of changes proposed in this pull request - Added cleanupIntegrityKeyTag if `getWalletAttestation` throws an error ## How to test Ensure that, when creating a WI, after an error on the `WalletInstanceAttestationObtainment` step, the `IntegrityKeyTag` has been correctly deleted, and a new key is generated when retrying to create a WI. --- .../common/utils/itwAttestationUtils.ts | 3 ++ .../machine/eid/__tests__/machine.test.ts | 47 +++++++++++++++++++ ts/features/itwallet/machine/eid/actions.ts | 10 +++- ts/features/itwallet/machine/eid/machine.ts | 3 +- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/ts/features/itwallet/common/utils/itwAttestationUtils.ts b/ts/features/itwallet/common/utils/itwAttestationUtils.ts index a28f06739b5..985163f6dfb 100644 --- a/ts/features/itwallet/common/utils/itwAttestationUtils.ts +++ b/ts/features/itwallet/common/utils/itwAttestationUtils.ts @@ -23,6 +23,7 @@ export const getIntegrityHardwareKeyTag = async (): Promise => /** * Register a new wallet instance with hardwareKeyTag. * @param hardwareKeyTag - the hardware key tag of the integrity Context + * @param sessionToken - the session token to use for the API calls */ export const registerWalletInstance = async ( hardwareKeyTag: string, @@ -42,6 +43,7 @@ export const registerWalletInstance = async ( /** * Getter for the wallet attestation binded to the wallet instance created with the given hardwareKeyTag. * @param hardwareKeyTag - the hardware key tag of the wallet instance + * @param sessionToken - the session token to use for the API calls * @return the wallet attestation and the related key tag */ export const getAttestation = async ( @@ -81,6 +83,7 @@ export const isWalletInstanceAttestationValid = ( * Get the wallet instance status from the Wallet Provider. * This operation is more lightweight than getting a new attestation to check the status. * @param hardwareKeyTag The hardware key tag used to create the wallet instance + * @param sessionToken The session token to use for the API calls */ export const getWalletInstanceStatus = ( hardwareKeyTag: string, diff --git a/ts/features/itwallet/machine/eid/__tests__/machine.test.ts b/ts/features/itwallet/machine/eid/__tests__/machine.test.ts index c7d51a9c7ab..751df57822b 100644 --- a/ts/features/itwallet/machine/eid/__tests__/machine.test.ts +++ b/ts/features/itwallet/machine/eid/__tests__/machine.test.ts @@ -40,6 +40,7 @@ describe("itwEidIssuanceMachine", () => { const navigateToNfcInstructionsScreen = jest.fn(); const navigateToCieIdLoginScreen = jest.fn(); const storeIntegrityKeyTag = jest.fn(); + const cleanupIntegrityKeyTag = jest.fn(); const storeWalletInstanceAttestation = jest.fn(); const storeEidCredential = jest.fn(); const closeIssuance = jest.fn(); @@ -82,6 +83,7 @@ describe("itwEidIssuanceMachine", () => { navigateToNfcInstructionsScreen, navigateToCieIdLoginScreen, storeIntegrityKeyTag, + cleanupIntegrityKeyTag, storeWalletInstanceAttestation, storeEidCredential, closeIssuance, @@ -982,4 +984,49 @@ describe("itwEidIssuanceMachine", () => { expect(actor.getSnapshot().value).toStrictEqual("Idle"); }); + + it("should cleanup integrity key tag and fail when obtaining Wallet Instance Attestation fails", async () => { + const actor = createActor(mockedMachine); + actor.start(); + + await waitFor(() => expect(onInit).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toStrictEqual("Idle"); + expect(actor.getSnapshot().context).toStrictEqual(InitialContext); + expect(actor.getSnapshot().tags).toStrictEqual(new Set()); + + /** + * Start eID issuance + */ + actor.send({ type: "start" }); + + expect(actor.getSnapshot().value).toStrictEqual("TosAcceptance"); + expect(actor.getSnapshot().tags).toStrictEqual(new Set()); + expect(navigateToTosScreen).toHaveBeenCalledTimes(1); + + /** + * Accept TOS and request WIA + */ + + createWalletInstance.mockImplementation(() => + Promise.resolve(T_INTEGRITY_KEY) + ); + getWalletAttestation.mockImplementation(() => Promise.reject({})); // Simulate failure + isSessionExpired.mockImplementation(() => false); // Session not expired + + actor.send({ type: "accept-tos" }); + + expect(actor.getSnapshot().value).toStrictEqual("WalletInstanceCreation"); + expect(actor.getSnapshot().tags).toStrictEqual(new Set([ItwTags.Loading])); + + await waitFor(() => expect(createWalletInstance).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(getWalletAttestation).toHaveBeenCalledTimes(1)); + + // Wallet Instance Attestation failure triggers cleanupIntegrityKeyTag + expect(cleanupIntegrityKeyTag).toHaveBeenCalledTimes(1); + + // Check that the machine transitions to Failure state + expect(actor.getSnapshot().value).toStrictEqual("Failure"); + expect(actor.getSnapshot().tags).toStrictEqual(new Set()); + }); }); diff --git a/ts/features/itwallet/machine/eid/actions.ts b/ts/features/itwallet/machine/eid/actions.ts index 9b84d7a1e23..2572a537d9f 100644 --- a/ts/features/itwallet/machine/eid/actions.ts +++ b/ts/features/itwallet/machine/eid/actions.ts @@ -9,7 +9,10 @@ import { checkCurrentSession } from "../../../../store/actions/authentication"; import { useIOStore } from "../../../../store/hooks"; import { assert } from "../../../../utils/assert"; import { itwCredentialsStore } from "../../credentials/store/actions"; -import { itwStoreIntegrityKeyTag } from "../../issuance/store/actions"; +import { + itwRemoveIntegrityKeyTag, + itwStoreIntegrityKeyTag +} from "../../issuance/store/actions"; import { itwLifecycleStateUpdated, itwLifecycleWalletReset @@ -168,6 +171,11 @@ export const createEidIssuanceActionsImplementation = ( store.dispatch(itwStoreIntegrityKeyTag(context.integrityKeyTag)); }, + cleanupIntegrityKeyTag: () => { + // Remove the integrity key tag from the store + store.dispatch(itwRemoveIntegrityKeyTag()); + }, + storeWalletInstanceAttestation: ({ context }: ActionArgs) => { diff --git a/ts/features/itwallet/machine/eid/machine.ts b/ts/features/itwallet/machine/eid/machine.ts index 3da95b4605c..1d381db258f 100644 --- a/ts/features/itwallet/machine/eid/machine.ts +++ b/ts/features/itwallet/machine/eid/machine.ts @@ -43,6 +43,7 @@ export const itwEidIssuanceMachine = setup({ navigateToNfcInstructionsScreen: notImplemented, navigateToWalletRevocationScreen: notImplemented, storeIntegrityKeyTag: notImplemented, + cleanupIntegrityKeyTag: notImplemented, storeWalletInstanceAttestation: notImplemented, storeEidCredential: notImplemented, closeIssuance: notImplemented, @@ -225,7 +226,7 @@ export const itwEidIssuanceMachine = setup({ target: "#itwEidIssuanceMachine.TosAcceptance" }, { - actions: "setFailure", + actions: ["setFailure", "cleanupIntegrityKeyTag"], target: "#itwEidIssuanceMachine.Failure" } ] From 9321272753c124be4627f8cd00830e2d591f520d Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Fri, 20 Dec 2024 15:06:49 +0100 Subject: [PATCH 7/9] chore(IT Wallet): [SIW-1937] Improve wallet category filters (#6570) ## Short description This PR fixes a bug in the wallet screen where users were unable to see any screen content if the category filters were in the wrong state. This PR also improves the debug data overlay and the wallet redux selectors. ### Steps to reproduce the addressed bug - Navigate to the Wallet screen, ensure you have *Documenti su IO* enabled and at least one payment method saved - Select the "Other" category filter - Remove everything but ITW from the wallet, keeping the "Other" filter selected - Once you removed everything you should see a blank screen - Restarting the app should not fix the issue ## List of changes proposed in this pull request - Added `isWalletCategoryFilteringEnabledSelector` selector to check if the category filtering is available: filtering is available only if there is more than one category in the wallet. - Added `shouldRenderWalletCategorySelector` selector to check if a wallet category section should be rendered, based on the currently selected filter and the number of categories available in the wallet - Added `withWalletCategoryFilter` HOC, which display a component based on the given category filter - Added `selectWalletCardsByCategory` and `selectWalletCardsByType` to select cards based on category or type, removing the need to have dedicated selectors - Removed `selectWalletCgnCard` and `selectBonusCards` selectors - Improved debug data in `DebugInfoOverlay` - Added the ability to display `Set` objects - Added the ability to add data from different components at the same time by merging data using the same key - Improved wallet category filtering - Refactored `WalletCardsContainer` with new selectors - Added useful/missing debug data - Added tests - Removed `categoryFilter` preference persistence from the `wallet.preferences` feature reducer ## How to test - Static checks should pass - Navigate to the wallet screen and check that everything works fine, especially the category filters - Try to reproduce the bug, you should now see the wallet content. --- ts/components/debug/__tests__/utils.test.ts | 17 ++ ts/components/debug/utils.ts | 15 +- ts/components/debug/withDebugEnabled.tsx | 6 +- .../screens/WalletCardOnboardingScreen.tsx | 8 +- .../components/WalletCardsContainer.tsx | 66 ++++--- .../components/WalletCategoryFilterTabs.tsx | 24 ++- .../__tests__/WalletCardsContainer.test.tsx | 12 +- .../WalletCategoryFilterTabs.test.tsx | 16 +- ts/features/wallet/saga/index.ts | 4 +- .../__tests__/WalletHomeScreen.test.tsx | 99 +++------- .../wallet/store/reducers/preferences.ts | 24 ++- .../store/selectors/__tests__/index.test.ts | 174 ++++++++++++++++-- ts/features/wallet/store/selectors/index.ts | 114 ++++++++---- .../wallet/utils/__tests__/index.test.tsx | 40 ++++ ts/features/wallet/utils/index.tsx | 32 +++- ts/mixpanelConfig/mixpanelPropertyUtils.ts | 14 +- ts/store/reducers/debug.ts | 3 +- 17 files changed, 469 insertions(+), 199 deletions(-) create mode 100644 ts/components/debug/__tests__/utils.test.ts create mode 100644 ts/features/wallet/utils/__tests__/index.test.tsx diff --git a/ts/components/debug/__tests__/utils.test.ts b/ts/components/debug/__tests__/utils.test.ts new file mode 100644 index 00000000000..a7c472c8449 --- /dev/null +++ b/ts/components/debug/__tests__/utils.test.ts @@ -0,0 +1,17 @@ +import { truncateObjectStrings } from "../utils"; + +describe("truncateObjectStrings", () => { + it.each` + input | maxLength | expected + ${"Long string"} | ${4} | ${"Long..."} + ${{ outer: { inner: "Long string" }, bool: true }} | ${4} | ${{ outer: { inner: "Long..." }, bool: true }} + ${["Long string", "Very long string"]} | ${4} | ${["Long...", "Very..."]} + ${new Set(["Long string", "Very long string"])} | ${4} | ${["Long...", "Very..."]} + `( + "$input should be truncated to $expected", + ({ input, maxLength, expected }) => { + const result = truncateObjectStrings(input, maxLength); + expect(result).toEqual(expected); + } + ); +}); diff --git a/ts/components/debug/utils.ts b/ts/components/debug/utils.ts index 0057ede6745..698c8cd51d0 100644 --- a/ts/components/debug/utils.ts +++ b/ts/components/debug/utils.ts @@ -1,12 +1,17 @@ type Primitive = string | number | boolean | null | undefined; -type TruncatableValue = Primitive | TruncatableObject | TruncatableArray; +type TruncatableValue = + | Primitive + | TruncatableObject + | TruncatableArray + | TruncatableSet; interface TruncatableObject { [key: string]: TruncatableValue; } type TruncatableArray = Array; +type TruncatableSet = Set; /** * Truncates all string values in an object or array structure to a specified maximum length. @@ -37,6 +42,14 @@ export const truncateObjectStrings = ( } if (typeof value === "object" && value !== null) { + if (value instanceof Set) { + // Set could not be serialized to JSON because values are not stored as properties + // For display purposes, we convert it to an array + return Array.from(value).map(item => + truncateObjectStrings(item, maxLength) + ) as T; + } + return Object.entries(value).reduce( (acc, [key, val]) => ({ ...acc, diff --git a/ts/components/debug/withDebugEnabled.tsx b/ts/components/debug/withDebugEnabled.tsx index 72eb5eccea5..303a6aa45dd 100644 --- a/ts/components/debug/withDebugEnabled.tsx +++ b/ts/components/debug/withDebugEnabled.tsx @@ -6,11 +6,13 @@ import { isDebugModeEnabledSelector } from "../../store/reducers/debug"; * This HOC allows to render the wrapped component only if the debug mode is enabled, otherwise returns null (nothing) */ export const withDebugEnabled = - (WrappedComponent: React.ComponentType

) => +

>( + WrappedComponent: React.ComponentType

+ ) => (props: P) => { const isDebug = useIOSelector(isDebugModeEnabledSelector); if (!isDebug) { return null; } - return ; + return ; }; diff --git a/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx b/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx index 0efb8a6768f..cdaf466893f 100644 --- a/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx +++ b/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx @@ -112,7 +112,7 @@ const ItwCredentialOnboardingSection = () => { ); return ( - <> + @@ -134,7 +134,7 @@ const ItwCredentialOnboardingSection = () => { /> ))} - + ); }; @@ -175,7 +175,7 @@ const OtherCardsOnboardingSection = (props: { showTitle?: boolean }) => { ); return ( - <> + {props.showTitle && ( { onPress={navigateToPaymentMethodOnboarding} /> - + ); }; diff --git a/ts/features/wallet/components/WalletCardsContainer.tsx b/ts/features/wallet/components/WalletCardsContainer.tsx index 9d08e1c2a8b..562c74fec75 100644 --- a/ts/features/wallet/components/WalletCardsContainer.tsx +++ b/ts/features/wallet/components/WalletCardsContainer.tsx @@ -3,12 +3,12 @@ import { useFocusEffect } from "@react-navigation/native"; import * as React from "react"; import { View } from "react-native"; import Animated, { LinearTransition } from "react-native-reanimated"; +import { useDebugInfo } from "../../../hooks/useDebugInfo"; import I18n from "../../../i18n"; import { useIONavigation } from "../../../navigation/params/AppParamsList"; import { useIOSelector } from "../../../store/hooks"; import { isItwEnabledSelector } from "../../../store/reducers/backendStatus/remoteConfig"; import { useIOBottomSheetAutoresizableModal } from "../../../utils/hooks/bottomSheet"; -import { ItwDiscoveryBannerStandalone } from "../../itwallet/common/components/discoveryBanner/ItwDiscoveryBannerStandalone"; import { ItwEidInfoBottomSheetContent, ItwEidInfoBottomSheetTitle @@ -16,22 +16,22 @@ import { import { ItwEidLifecycleAlert } from "../../itwallet/common/components/ItwEidLifecycleAlert"; import { ItwFeedbackBanner } from "../../itwallet/common/components/ItwFeedbackBanner"; import { ItwWalletReadyBanner } from "../../itwallet/common/components/ItwWalletReadyBanner"; +import { ItwDiscoveryBannerStandalone } from "../../itwallet/common/components/discoveryBanner/ItwDiscoveryBannerStandalone"; import { itwCredentialsEidStatusSelector } from "../../itwallet/credentials/store/selectors"; import { itwLifecycleIsValidSelector } from "../../itwallet/lifecycle/store/selectors"; +import { useItwWalletInstanceRevocationAlert } from "../../itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert"; import { isWalletEmptySelector, - selectIsWalletCardsLoading, + selectIsWalletLoading, + selectWalletCardsByCategory, selectWalletCategories, - selectWalletCategoryFilter, - selectWalletItwCards, selectWalletOtherCards, shouldRenderWalletEmptyStateSelector } from "../store/selectors"; -import { WalletCardCategoryFilter } from "../types"; -import { useItwWalletInstanceRevocationAlert } from "../../itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert"; +import { withWalletCategoryFilter } from "../utils"; +import { WalletCardSkeleton } from "./WalletCardSkeleton"; import { WalletCardsCategoryContainer } from "./WalletCardsCategoryContainer"; import { WalletCardsCategoryRetryErrorBanner } from "./WalletCardsCategoryRetryErrorBanner"; -import { WalletCardSkeleton } from "./WalletCardSkeleton"; import { WalletEmptyScreenContent } from "./WalletEmptyScreenContent"; const EID_INFO_BOTTOM_PADDING = 128; @@ -42,9 +42,8 @@ const EID_INFO_BOTTOM_PADDING = 128; * and the empty state */ const WalletCardsContainer = () => { - const isLoading = useIOSelector(selectIsWalletCardsLoading); + const isLoading = useIOSelector(selectIsWalletLoading); const isWalletEmpty = useIOSelector(isWalletEmptySelector); - const selectedCategory = useIOSelector(selectWalletCategoryFilter); const shouldRenderEmptyState = useIOSelector( shouldRenderWalletEmptyStateSelector ); @@ -55,13 +54,6 @@ const WalletCardsContainer = () => { // placeholders in the wallet const shouldRenderLoadingState = isLoading && isWalletEmpty; - // Returns true if no category filter is selected or if the filter matches the given category - const shouldRenderCategory = React.useCallback( - (filter: WalletCardCategoryFilter): boolean => - selectedCategory === undefined || selectedCategory === filter, - [selectedCategory] - ); - // Content to render in the wallet screen, based on the current state const walletContent = React.useMemo(() => { if (shouldRenderLoadingState) { @@ -72,11 +64,11 @@ const WalletCardsContainer = () => { } return ( - {shouldRenderCategory("itw") && } - {shouldRenderCategory("other") && } + + ); - }, [shouldRenderEmptyState, shouldRenderCategory, shouldRenderLoadingState]); + }, [shouldRenderEmptyState, shouldRenderLoadingState]); return ( { ); }; +/** + * Skeleton for the wallet cards container + */ const WalletCardsContainerSkeleton = () => ( <> @@ -97,15 +92,29 @@ const WalletCardsContainerSkeleton = () => ( ); -const ItwWalletCardsContainer = () => { +/** + * Card container for the ITW credentials + */ +const ItwWalletCardsContainer = withWalletCategoryFilter("itw", () => { const navigation = useIONavigation(); - const cards = useIOSelector(selectWalletItwCards); + const cards = useIOSelector(state => + selectWalletCardsByCategory(state, "itw") + ); const isItwValid = useIOSelector(itwLifecycleIsValidSelector); const isItwEnabled = useIOSelector(isItwEnabledSelector); const eidStatus = useIOSelector(itwCredentialsEidStatusSelector); const isEidExpired = eidStatus === "jwtExpired"; + useDebugInfo({ + itw: { + isItwValid, + isItwEnabled, + eidStatus, + cards + } + }); + const eidInfoBottomSheet = useIOBottomSheetAutoresizableModal( { title: , @@ -172,12 +181,21 @@ const ItwWalletCardsContainer = () => { {isItwValid && eidInfoBottomSheet.bottomSheet} ); -}; +}); -const OtherWalletCardsContainer = () => { +/** + * Card container for the other cards (payments, bonus, etc.) + */ +const OtherWalletCardsContainer = withWalletCategoryFilter("other", () => { const cards = useIOSelector(selectWalletOtherCards); const categories = useIOSelector(selectWalletCategories); + useDebugInfo({ + other: { + cards + } + }); + const sectionHeader = React.useMemo((): ListItemHeader | undefined => { // The section header must be displayed only if there are more categories if (categories.size <= 1) { @@ -203,7 +221,7 @@ const OtherWalletCardsContainer = () => { bottomElement={} /> ); -}; +}); export { ItwWalletCardsContainer, diff --git a/ts/features/wallet/components/WalletCategoryFilterTabs.tsx b/ts/features/wallet/components/WalletCategoryFilterTabs.tsx index 9cec1e0534e..10b0921f266 100644 --- a/ts/features/wallet/components/WalletCategoryFilterTabs.tsx +++ b/ts/features/wallet/components/WalletCategoryFilterTabs.tsx @@ -10,10 +10,11 @@ import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { trackWalletCategoryFilter } from "../../itwallet/analytics"; import { walletSetCategoryFilter } from "../store/actions/preferences"; import { - selectWalletCategories, + isWalletCategoryFilteringEnabledSelector, selectWalletCategoryFilter } from "../store/selectors"; import { walletCardCategoryFilters } from "../types"; +import { useDebugInfo } from "../../../hooks/useDebugInfo"; /** * Renders filter tabs to categorize cards on the wallet home screen. @@ -23,18 +24,27 @@ import { walletCardCategoryFilters } from "../types"; const WalletCategoryFilterTabs = () => { const dispatch = useIODispatch(); - const selectedCategory = useIOSelector(selectWalletCategoryFilter); - const categories = useIOSelector(selectWalletCategories); + const categoryFilter = useIOSelector(selectWalletCategoryFilter); + const isFilteringEnabled = useIOSelector( + isWalletCategoryFilteringEnabledSelector + ); + + useDebugInfo({ + wallet: { + isFilteringEnabled, + categoryFilter + } + }); const selectedIndex = React.useMemo( () => - selectedCategory - ? walletCardCategoryFilters.indexOf(selectedCategory) + 1 + categoryFilter + ? walletCardCategoryFilters.indexOf(categoryFilter) + 1 : 0, - [selectedCategory] + [categoryFilter] ); - if (categories.size <= 1) { + if (!isFilteringEnabled) { return null; } diff --git a/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx b/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx index c364c9f9a4f..13b5dec74bd 100644 --- a/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx +++ b/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx @@ -95,7 +95,7 @@ describe("WalletCardsContainer", () => { it("should render the loading screen", () => { jest - .spyOn(walletSelectors, "selectIsWalletCardsLoading") + .spyOn(walletSelectors, "selectIsWalletLoading") .mockImplementation(() => true); jest .spyOn(walletSelectors, "selectWalletCategoryFilter") @@ -117,7 +117,7 @@ describe("WalletCardsContainer", () => { it("should render the empty screen", () => { jest - .spyOn(walletSelectors, "selectIsWalletCardsLoading") + .spyOn(walletSelectors, "selectIsWalletLoading") .mockImplementation(() => false); jest .spyOn(walletSelectors, "selectWalletCategoryFilter") @@ -150,14 +150,14 @@ describe("WalletCardsContainer", () => { .mockImplementation(() => [T_CARDS["1"], T_CARDS["2"], T_CARDS["3"]]); jest - .spyOn(walletSelectors, "selectWalletItwCards") + .spyOn(walletSelectors, "selectWalletCardsByCategory") .mockImplementation(() => [T_CARDS["4"], T_CARDS["5"]]); jest .spyOn(configSelectors, "isItwEnabledSelector") .mockImplementation(() => true); jest - .spyOn(walletSelectors, "selectIsWalletCardsLoading") + .spyOn(walletSelectors, "selectIsWalletLoading") .mockImplementation(() => false); jest .spyOn(walletSelectors, "selectWalletCategoryFilter") @@ -193,7 +193,7 @@ describe("WalletCardsContainer", () => { .mockImplementation(() => true); jest - .spyOn(walletSelectors, "selectIsWalletCardsLoading") + .spyOn(walletSelectors, "selectIsWalletLoading") .mockImplementation(() => isLoading); jest .spyOn(walletSelectors, "shouldRenderWalletEmptyStateSelector") @@ -244,7 +244,7 @@ describe("ItwWalletCardsContainer", () => { .spyOn(configSelectors, "isItwEnabledSelector") .mockImplementation(() => true); jest - .spyOn(walletSelectors, "selectWalletItwCards") + .spyOn(walletSelectors, "selectWalletCardsByCategory") .mockImplementation(() => [T_CARDS["4"], T_CARDS["5"]]); const { queryByTestId } = renderComponent(ItwWalletCardsContainer); diff --git a/ts/features/wallet/components/__tests__/WalletCategoryFilterTabs.test.tsx b/ts/features/wallet/components/__tests__/WalletCategoryFilterTabs.test.tsx index 06452f7ab0c..9740c683227 100644 --- a/ts/features/wallet/components/__tests__/WalletCategoryFilterTabs.test.tsx +++ b/ts/features/wallet/components/__tests__/WalletCategoryFilterTabs.test.tsx @@ -12,25 +12,25 @@ import * as selectors from "../../store/selectors"; import { WalletCategoryFilterTabs } from "../WalletCategoryFilterTabs"; describe("WalletCategoryFilterTabs", () => { - it("should not render the component if there is only one cards category in the wallet", () => { + it("should not render the component if category filtering is not enabled", () => { jest .spyOn(selectors, "selectWalletCategoryFilter") .mockImplementation(() => undefined); jest - .spyOn(selectors, "selectWalletCategories") - .mockImplementation(() => new Set(["itw"])); + .spyOn(selectors, "isWalletCategoryFilteringEnabledSelector") + .mockImplementation(() => false); const { queryByTestId } = renderComponent(); expect(queryByTestId("CategoryTabsContainerTestID")).toBeNull(); }); - it("should render the component if there is more than one cards category in the wallet", () => { + it("should render the component if category filtering is enabled", () => { jest .spyOn(selectors, "selectWalletCategoryFilter") .mockImplementation(() => undefined); jest - .spyOn(selectors, "selectWalletCategories") - .mockImplementation(() => new Set(["itw", "other"])); + .spyOn(selectors, "isWalletCategoryFilteringEnabledSelector") + .mockImplementation(() => true); const { queryByTestId } = renderComponent(); expect(queryByTestId("CategoryTabsContainerTestID")).not.toBeNull(); @@ -45,8 +45,8 @@ describe("WalletCategoryFilterTabs", () => { .spyOn(selectors, "selectWalletCategoryFilter") .mockImplementation(() => undefined); jest - .spyOn(selectors, "selectWalletCategories") - .mockImplementation(() => new Set(["itw", "other"])); + .spyOn(selectors, "isWalletCategoryFilteringEnabledSelector") + .mockImplementation(() => true); const { getByTestId } = renderComponent(); const itwTab = getByTestId("CategoryTabTestID-itw"); diff --git a/ts/features/wallet/saga/index.ts b/ts/features/wallet/saga/index.ts index 5511df75f9b..36eb1639359 100644 --- a/ts/features/wallet/saga/index.ts +++ b/ts/features/wallet/saga/index.ts @@ -10,7 +10,7 @@ import { import { walletUpdate } from "../store/actions"; import { walletAddCards } from "../store/actions/cards"; import { walletToggleLoadingState } from "../store/actions/placeholders"; -import { selectWalletPlaceholders } from "../store/selectors"; +import { selectWalletPlaceholderCards } from "../store/selectors"; import { handleWalletAnalyticsSaga } from "./handleWalletAnalyticsSaga"; import { handleWalletPlaceholdersTimeout } from "./handleWalletLoadingPlaceholdersTimeout"; import { handleWalletLoadingStateSaga } from "./handleWalletLoadingStateSaga"; @@ -21,7 +21,7 @@ const LOADING_STATE_TIMEOUT = 2000 as Millisecond; export function* watchWalletSaga(): SagaIterator { // Adds persisted placeholders as cards in the wallet // to be displayed while waiting for the actual cards - const placeholders = yield* select(selectWalletPlaceholders); + const placeholders = yield* select(selectWalletPlaceholderCards); yield* put(walletAddCards(placeholders)); yield* takeLatest( diff --git a/ts/features/wallet/screens/__tests__/WalletHomeScreen.test.tsx b/ts/features/wallet/screens/__tests__/WalletHomeScreen.test.tsx index 6bae047df8c..048de392d91 100644 --- a/ts/features/wallet/screens/__tests__/WalletHomeScreen.test.tsx +++ b/ts/features/wallet/screens/__tests__/WalletHomeScreen.test.tsx @@ -1,12 +1,10 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import _ from "lodash"; import configureMockStore from "redux-mock-store"; import ROUTES from "../../../../navigation/routes"; import { applicationChangeState } from "../../../../store/actions/application"; import { appReducer } from "../../../../store/reducers"; import { GlobalState } from "../../../../store/reducers/types"; import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; -import { WalletCardsState } from "../../store/reducers/cards"; +import * as walletSelectors from "../../store/selectors"; import { WalletHomeScreen } from "../WalletHomeScreen"; jest.mock("react-native-reanimated", () => ({ @@ -18,98 +16,45 @@ jest.mock("react-native-reanimated", () => ({ } })); -const T_CARDS: WalletCardsState = { - "1": { - key: "1", - type: "payment", - category: "payment", - walletId: "" - }, - "2": { - key: "2", - type: "payment", - category: "payment", - walletId: "" - }, - "3": { - key: "3", - type: "idPay", - category: "bonus", - amount: 1234, - avatarSource: { - uri: "" - }, - expireDate: new Date(), - initiativeId: "", - name: "ABC" - } -}; - describe("WalletHomeScreen", () => { jest.useFakeTimers(); jest.runAllTimers(); - it("should correctly render empty screen", () => { - const { - component: { queryByTestId } - } = renderComponent({}); + it("should not render screen actions if the wallet is empty", () => { + jest + .spyOn(walletSelectors, "isWalletEmptySelector") + .mockImplementation(() => true); + + const { queryByTestId } = renderComponent(); jest.runOnlyPendingTimers(); - expect(queryByTestId("walletPaymentsRedirectBannerTestID")).toBeNull(); - expect(queryByTestId("walletEmptyScreenContentTestID")).not.toBeNull(); - expect(queryByTestId("walletCardsContainerTestID")).toBeNull(); expect(queryByTestId("walletAddCardButtonTestID")).toBeNull(); }); - it("should correctly render card list screen", () => { - const { - component: { queryByTestId } - } = renderComponent(T_CARDS); + it("should render screen actions if the wallet is not empty", () => { + jest + .spyOn(walletSelectors, "isWalletEmptySelector") + .mockImplementation(() => false); + + const { queryByTestId } = renderComponent(); + + jest.runOnlyPendingTimers(); - expect(queryByTestId("walletPaymentsRedirectBannerTestID")).toBeNull(); - expect(queryByTestId("walletEmptyScreenContentTestID")).toBeNull(); - expect(queryByTestId("walletCardsContainerTestID")).not.toBeNull(); expect(queryByTestId("walletAddCardButtonTestID")).not.toBeNull(); }); }); -const renderComponent = ( - cards: WalletCardsState, - options: { - isLoading?: boolean; - } = {} -) => { +const renderComponent = () => { const globalState = appReducer(undefined, applicationChangeState("active")); - const { isLoading = false } = options; const mockStore = configureMockStore(); - const store: ReturnType = mockStore( - _.merge(globalState, { - features: { - wallet: { - cards, - preferences: {}, - placeholders: { - isLoading - } - }, - payments: { - wallet: { - userMethods: pot.some([]) - } - } - } - }) - ); + const store: ReturnType = mockStore(globalState); - return { - component: renderScreenWithNavigationStoreContext( - WalletHomeScreen, - ROUTES.WALLET_HOME, - {}, - store - ), + return renderScreenWithNavigationStoreContext( + WalletHomeScreen, + ROUTES.WALLET_HOME, + {}, store - }; + ); }; diff --git a/ts/features/wallet/store/reducers/preferences.ts b/ts/features/wallet/store/reducers/preferences.ts index 49cf5f2c708..01b39b1dc87 100644 --- a/ts/features/wallet/store/reducers/preferences.ts +++ b/ts/features/wallet/store/reducers/preferences.ts @@ -1,9 +1,17 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; -import { PersistConfig, persistReducer } from "redux-persist"; +import _ from "lodash"; +import { + MigrationManifest, + PersistConfig, + PersistedState, + createMigrate, + persistReducer +} from "redux-persist"; import { getType } from "typesafe-actions"; import { Action } from "../../../../store/actions/types"; -import { walletSetCategoryFilter } from "../actions/preferences"; +import { isDevEnv } from "../../../../utils/environment"; import { WalletCardCategoryFilter } from "../../types"; +import { walletSetCategoryFilter } from "../actions/preferences"; export type WalletPreferencesState = { categoryFilter?: WalletCardCategoryFilter; @@ -25,12 +33,20 @@ const reducer = ( return state; }; -const CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION = -1; +const CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION = 0; + +const migrations: MigrationManifest = { + // Removed categoryFilter persistance requirement + "0": (state: PersistedState): PersistedState => + _.set(state, "preferences", {}) +}; const persistConfig: PersistConfig = { key: "walletPreferences", storage: AsyncStorage, - version: CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION + version: CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION, + blacklist: ["categoryFilter"], + migrate: createMigrate(migrations, { debug: isDevEnv }) }; export const walletReducerPersistor = persistReducer< diff --git a/ts/features/wallet/store/selectors/__tests__/index.test.ts b/ts/features/wallet/store/selectors/__tests__/index.test.ts index bf524fdbf2d..c1030f3dc6d 100644 --- a/ts/features/wallet/store/selectors/__tests__/index.test.ts +++ b/ts/features/wallet/store/selectors/__tests__/index.test.ts @@ -1,10 +1,14 @@ -import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; import _ from "lodash"; import { + isWalletCategoryFilteringEnabledSelector, isWalletEmptySelector, selectWalletCards, + selectWalletCardsByCategory, + selectWalletCardsByType, selectWalletCategories, + shouldRenderWalletCategorySelector, shouldRenderWalletEmptyStateSelector } from ".."; import { applicationChangeState } from "../../../../../store/actions/application"; @@ -15,9 +19,25 @@ import { } from "../../../../itwallet/common/utils/itwMocksUtils"; import { ItwLifecycleState } from "../../../../itwallet/lifecycle/store/reducers"; import * as itwLifecycleSelectors from "../../../../itwallet/lifecycle/store/selectors"; +import { walletCardCategoryFilters } from "../../../types"; import { WalletCardsState } from "../../reducers/cards"; -const T_CARDS: WalletCardsState = { +const T_ITW_CARDS: WalletCardsState = { + "4": { + key: "4", + category: "itw", + type: "itw", + credentialType: CredentialType.DRIVING_LICENSE + }, + "5": { + key: "5", + category: "itw", + type: "itw", + credentialType: CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD + } +}; + +const T_OTHER_CARDS: WalletCardsState = { "1": { key: "1", category: "payment", @@ -40,21 +60,14 @@ const T_CARDS: WalletCardsState = { key: "3", category: "cgn", type: "cgn" - }, - "4": { - key: "4", - category: "itw", - type: "itw", - credentialType: CredentialType.DRIVING_LICENSE - }, - "5": { - key: "5", - category: "itw", - type: "itw", - credentialType: CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD } }; +const T_CARDS: WalletCardsState = { + ...T_ITW_CARDS, + ...T_OTHER_CARDS +}; + describe("selectWalletCards", () => { it("should return the correct cards", () => { const globalState = appReducer(undefined, applicationChangeState("active")); @@ -128,6 +141,34 @@ describe("selectWalletCategories", () => { }); }); +describe("selectWalletCardsByType", () => { + it("should return the correct cards", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const cards = selectWalletCardsByType( + _.set(globalState, "features.wallet", { + cards: T_CARDS + }), + "idPay" + ); + expect(cards).toEqual([T_CARDS["2"]]); + }); +}); + +describe("selectWalletCardsByCategory", () => { + it("should return the correct cards", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const cards = selectWalletCardsByCategory( + _.set(globalState, "features.wallet", { + cards: T_CARDS + }), + "itw" + ); + expect(cards).toEqual([T_CARDS["4"], T_CARDS["5"]]); + }); +}); + describe("isWalletEmptySelector", () => { it("should return true if there are no categories to display", () => { const globalState = appReducer(undefined, applicationChangeState("active")); @@ -220,3 +261,108 @@ describe("shouldRenderWalletEmptyStateSelector", () => { } ); }); + +describe("isWalletCategoryFilteringEnabledSelector", () => { + it("should return true if the categories are ['itw', 'other']", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const isWalletCategoryFilteringEnabled = + isWalletCategoryFilteringEnabledSelector( + _.merge( + globalState, + _.set(globalState, "features.wallet", { + cards: T_CARDS + }) + ) + ); + + expect(isWalletCategoryFilteringEnabled).toBe(true); + }); + + it("should return false if the categories are ['itw']", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const isWalletCategoryFilteringEnabled = + isWalletCategoryFilteringEnabledSelector( + _.merge( + globalState, + _.set(globalState, "features.wallet", { + cards: T_ITW_CARDS + }) + ) + ); + + expect(isWalletCategoryFilteringEnabled).toBe(false); + }); +}); + +describe("shouldRenderWalletCategorySelector", () => { + it("should return true if the category filter is undefined", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const shouldRenderWalletCategory = shouldRenderWalletCategorySelector( + _.merge( + globalState, + _.set(globalState, "features.wallet", { + cards: T_CARDS, + preferences: { + categoryFilter: undefined + } + }) + ), + "itw" + ); + + expect(shouldRenderWalletCategory).toBe(true); + }); + + it.each(walletCardCategoryFilters)( + "should return true if the category filter matches the given category when the category is %s", + categoryFilter => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + + const shouldRenderWalletCategory = shouldRenderWalletCategorySelector( + _.merge( + globalState, + _.set(globalState, "features.wallet", { + cards: T_CARDS, + preferences: { + categoryFilter + } + }) + ), + categoryFilter + ); + + expect(shouldRenderWalletCategory).toBe(true); + } + ); + + it.each(walletCardCategoryFilters)( + "should return true if the category filtering is not enabled and the category filter is %s", + categoryFilter => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + + const shouldRenderWalletCategory = shouldRenderWalletCategorySelector( + _.merge( + globalState, + _.set(globalState, "features.wallet", { + cards: T_ITW_CARDS, + preferences: { + categoryFilter + } + }) + ), + categoryFilter + ); + + expect(shouldRenderWalletCategory).toBe(true); + } + ); +}); diff --git a/ts/features/wallet/store/selectors/index.ts b/ts/features/wallet/store/selectors/index.ts index 0fc70d8a3fd..5ee212b0cce 100644 --- a/ts/features/wallet/store/selectors/index.ts +++ b/ts/features/wallet/store/selectors/index.ts @@ -1,35 +1,31 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { createSelector } from "reselect"; import { GlobalState } from "../../../../store/reducers/types"; +import { isSomeLoadingOrSomeUpdating } from "../../../../utils/pot"; import { cgnDetailSelector } from "../../../bonus/cgn/store/reducers/details"; import { idPayWalletInitiativeListSelector } from "../../../idpay/wallet/store/reducers"; import { itwLifecycleIsValidSelector } from "../../../itwallet/lifecycle/store/selectors"; import { paymentsWalletUserMethodsSelector } from "../../../payments/wallet/store/selectors"; -import { WalletCard, walletCardCategories } from "../../types"; -import { isSomeLoadingOrSomeUpdating } from "../../../../utils/pot"; - -const selectWalletFeature = (state: GlobalState) => state.features.wallet; - -export const selectWalletPlaceholders = createSelector( - selectWalletFeature, - wallet => - Object.entries(wallet.placeholders.items).map( - ([key, category]) => - ({ key, category, type: "placeholder" } as WalletCard) - ) -); +import { + WalletCard, + WalletCardCategory, + WalletCardType, + walletCardCategories +} from "../../types"; +import { WalletCardCategoryFilter } from "../../types/index"; /** * Returns the list of cards excluding hidden cards */ export const selectWalletCards = createSelector( - selectWalletFeature, - ({ cards }) => Object.values(cards).filter(({ hidden }) => !hidden) + (state: GlobalState) => state.features.wallet.cards, + cards => Object.values(cards).filter(({ hidden }) => !hidden) ); /** * Returns the list of card categories available in the wallet * If there are categories other that ITW, they will become "other" + * If the ITW is valid, it will be counted as "itw" category, since we do not have eID card anymore */ export const selectWalletCategories = createSelector( selectWalletCards, @@ -66,47 +62,58 @@ export const selectSortedWalletCards = createSelector( ); /** - * Only gets cards which are part of the IT Wallet + * Selects the cards by their category + * @param category - The category of the cards to select */ -export const selectWalletItwCards = createSelector( +export const selectWalletCardsByCategory = createSelector( selectSortedWalletCards, - cards => cards.filter(({ category }) => category === "itw") + (_: GlobalState, category: WalletCardCategory) => category, + (cards, category) => + cards.filter(({ category: cardCategory }) => cardCategory === category) ); /** - * Only gets cards which are not part of the IT Wallet + * Selects the cards by their type + * @param type - The type of the cards to select */ -export const selectWalletOtherCards = createSelector( +export const selectWalletCardsByType = createSelector( selectSortedWalletCards, - cards => cards.filter(({ category }) => category !== "itw") + (_: GlobalState, type: WalletCardType) => type, + (cards, type) => cards.filter(({ type: cardType }) => cardType === type) ); -export const selectIsWalletCardsLoading = (state: GlobalState) => - state.features.wallet.placeholders.isLoading; - -export const selectWalletCategoryFilter = createSelector( - selectWalletFeature, - wallet => wallet.preferences.categoryFilter -); - -export const selectWalletPaymentMethods = createSelector( +/** + * Currently, if a card is not part of the IT Wallet, it is considered as "other" + * This selector returns the cards which are not part of the IT Wallet which must be displayed in the "other" section + */ +export const selectWalletOtherCards = createSelector( selectSortedWalletCards, - cards => cards.filter(({ category }) => category === "payment") + cards => cards.filter(({ category }) => category !== "itw") ); -export const selectWalletCgnCard = createSelector( - selectSortedWalletCards, - cards => cards.filter(({ category }) => category === "cgn") -); +/** + * Selects the loading state of the wallet cards + */ +export const selectIsWalletLoading = (state: GlobalState) => + state.features.wallet.placeholders.isLoading; -export const selectBonusCards = createSelector(selectSortedWalletCards, cards => - cards.filter(({ category }) => category === "bonus") +/** + * Selects the placeholders from the wallet + */ +export const selectWalletPlaceholderCards = createSelector( + (state: GlobalState) => state.features.wallet.placeholders.items, + placeholders => + Object.entries(placeholders).map( + ([key, category]) => + ({ key, category, type: "placeholder" } as WalletCard) + ) ); /** * Gets if the wallet can be considered empty. - * The wallet is empty if there are no categories to display - * @see selectWalletCategories + * The wallet is empty if there are no categories to display (@see selectWalletCategories) + * + * Note: we check categories because if ITW is valid, it is considered as one category even if there are no cards */ export const isWalletEmptySelector = (state: GlobalState) => selectWalletCategories(state).size === 0; @@ -127,3 +134,32 @@ export const isWalletScreenRefreshingSelector = (state: GlobalState) => isSomeLoadingOrSomeUpdating(paymentsWalletUserMethodsSelector(state)) || isSomeLoadingOrSomeUpdating(idPayWalletInitiativeListSelector(state)) || isSomeLoadingOrSomeUpdating(cgnDetailSelector(state)); + +/** + * Selects if the wallet categories can be filtered. + * The filter is only enabled if there are more than one category available + */ +export const isWalletCategoryFilteringEnabledSelector = createSelector( + selectWalletCategories, + categories => categories.size > 1 +); + +/** + * Selects the category filter from the wallet preferences + */ +export const selectWalletCategoryFilter = (state: GlobalState) => + state.features.wallet.preferences.categoryFilter; + +/** + * Checks if a wallet category section should be rendered. A category section is rendered if: + * - the category filtering is not enabled, or + * - no category filter is selected, or + * - the filter matches the given category + */ +export const shouldRenderWalletCategorySelector = createSelector( + isWalletCategoryFilteringEnabledSelector, + selectWalletCategoryFilter, + (_: GlobalState, category: WalletCardCategoryFilter) => category, + (isFilteringEnabled, filter, category) => + !isFilteringEnabled || filter === undefined || filter === category +); diff --git a/ts/features/wallet/utils/__tests__/index.test.tsx b/ts/features/wallet/utils/__tests__/index.test.tsx new file mode 100644 index 00000000000..534d6e66d93 --- /dev/null +++ b/ts/features/wallet/utils/__tests__/index.test.tsx @@ -0,0 +1,40 @@ +import { Body } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import configureMockStore from "redux-mock-store"; +import { withWalletCategoryFilter } from ".."; +import ROUTES from "../../../../navigation/routes"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { appReducer } from "../../../../store/reducers"; +import { GlobalState } from "../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import * as selectors from "../../store/selectors"; + +describe("withWalletCategoryFilter", () => { + it("should return null if the category filter does not match", () => { + const WrappedComponent = () => ( + Hello + ); + const ComponentWithFilter = withWalletCategoryFilter( + "itw", + WrappedComponent + ); + + const globalState = appReducer(undefined, applicationChangeState("active")); + + const mockStore = configureMockStore(); + const store: ReturnType = mockStore(globalState); + + jest + .spyOn(selectors, "shouldRenderWalletCategorySelector") + .mockImplementation(() => false); + + const { queryByTestId } = + renderScreenWithNavigationStoreContext( + () => , + ROUTES.WALLET_HOME, + {}, + store + ); + expect(queryByTestId("WrappedComponentTestID")).toBeNull(); + }); +}); diff --git a/ts/features/wallet/utils/index.tsx b/ts/features/wallet/utils/index.tsx index e2dad27dfdd..00a11ed90fe 100644 --- a/ts/features/wallet/utils/index.tsx +++ b/ts/features/wallet/utils/index.tsx @@ -1,11 +1,14 @@ import * as React from "react"; -import { WalletCard, WalletCardType } from "../types"; +import { WalletCard, WalletCardCategoryFilter, WalletCardType } from "../types"; import { WalletCardBaseComponent } from "../components/WalletCardBaseComponent"; import { CgnWalletCard } from "../../bonus/cgn/components/CgnWalletCard"; import { IdPayWalletCard } from "../../idpay/wallet/components/IdPayWalletCard"; import { PaymentWalletCard } from "../../payments/wallet/components/PaymentWalletCard"; import { WalletCardSkeleton } from "../components/WalletCardSkeleton"; import { ItwCredentialWalletCard } from "../../itwallet/wallet/components/ItwCredentialWalletCard"; +import { shouldRenderWalletCategorySelector } from "../store/selectors"; +import { useIOSelector } from "../../../store/hooks"; +import { GlobalState } from "../../../store/reducers/types"; /** * Wallet card component mapper which translates a WalletCardType to a @@ -24,6 +27,12 @@ export const walletCardComponentMapper: Record< placeholder: WalletCardSkeleton }; +/** + * Function that renders a wallet card using the mapped component inside {@see walletCardComponentMapper} + * @param card - The wallet card object to render + * @param stacked - Whether the card is stacked or not + * @returns The rendered card or null if the card is not found + */ export const renderWalletCardFn = ( card: WalletCard, stacked: boolean = false @@ -39,3 +48,24 @@ export const renderWalletCardFn = ( /> ) : null; }; + +/** + * A higher-order component which renders a component only if the category filter matches the given category + * @param category - The category to filter by + * @param WrappedComponent - The component to render + * @returns The component or null if the category filter does not match + */ +export const withWalletCategoryFilter = +

>( + category: WalletCardCategoryFilter, + WrappedComponent: React.ComponentType

+ ) => + (props: P) => { + const shouldRenderCategory = useIOSelector((state: GlobalState) => + shouldRenderWalletCategorySelector(state, category) + ); + if (!shouldRenderCategory) { + return null; + } + return ; + }; diff --git a/ts/mixpanelConfig/mixpanelPropertyUtils.ts b/ts/mixpanelConfig/mixpanelPropertyUtils.ts index 1d0617f2898..19b3fbe8ed8 100644 --- a/ts/mixpanelConfig/mixpanelPropertyUtils.ts +++ b/ts/mixpanelConfig/mixpanelPropertyUtils.ts @@ -4,10 +4,7 @@ import { ServicesPreferencesModeEnum } from "../../definitions/backend/ServicesP import { TrackCgnStatus } from "../features/bonus/cgn/analytics"; import { LoginSessionDuration } from "../features/fastLogin/analytics/optinAnalytics"; import { fastLoginOptInSelector } from "../features/fastLogin/store/selectors"; -import { - selectBonusCards, - selectWalletCgnCard -} from "../features/wallet/store/selectors"; +import { selectWalletCardsByType } from "../features/wallet/store/selectors"; import { WalletCardBonus } from "../features/wallet/types"; import { paymentsWalletUserMethodsSelector } from "../features/payments/wallet/store/selectors"; import { @@ -91,17 +88,16 @@ export const paymentMethodsHandler = (state: GlobalState): number | undefined => paymentsWalletUserMethodsNumberFromPotSelector(state)?.length; export const cgnStatusHandler = (state: GlobalState): TrackCgnStatus => { - const cgnCard = selectWalletCgnCard(state); + const cgnCard = selectWalletCardsByType(state, "cgn"); return cgnCard.length > 0 ? "active" : "not_active"; }; export const welfareStatusHandler = ( state: GlobalState ): ReadonlyArray => { - const bonusCards = selectBonusCards(state); - const idPayCards = bonusCards.filter( - card => card.type === "idPay" + const idPayCards = selectWalletCardsByType( + state, + "idPay" ) as Array; - return idPayCards.map(card => card.name); }; diff --git a/ts/store/reducers/debug.ts b/ts/store/reducers/debug.ts index 79b127ba1ac..99c957ee9a4 100644 --- a/ts/store/reducers/debug.ts +++ b/ts/store/reducers/debug.ts @@ -1,4 +1,5 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; +import _ from "lodash"; import { PersistConfig, PersistPartial, persistReducer } from "redux-persist"; import { getType } from "typesafe-actions"; import { @@ -37,7 +38,7 @@ function debugReducer( case getType(setDebugData): return { ...state, - debugData: action.payload + debugData: _.merge(state.debugData, action.payload) }; case getType(resetDebugData): return { From f739c1008302f41e5a1b7da9d1466a64699b3ca4 Mon Sep 17 00:00:00 2001 From: Emanuele Dall'Ara <71103219+LeleDallas@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:47:18 +0100 Subject: [PATCH 8/9] refactor: [IOBP-1091] Paid in app empty state screen (#6575) ## Short description This pull request includes updates to localization files and enhancements to the `ReceiptListScreen` component. The most important changes is refactoring the empty state logic in the receipt list screen. ## List of changes proposed in this pull request - Refactored the empty state logic to use a new `emptyProps` object based on the `noticeCategory`. - Added `emptyPayer` locale for displaying payment receipts made in the app. ## How to test - In `Pagamenti` tap on `Vedi tutte` - Ensure that the `Pagate in app` tab is display correct empty state ## Preview https://github.com/user-attachments/assets/8501fe55-1370-4613-9543-b929aa9907d6 --- locales/en/index.yml | 2 + locales/it/index.yml | 2 + .../receipts/screens/ReceiptListScreen.tsx | 56 ++++++++++++------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/locales/en/index.yml b/locales/en/index.yml index a10530a05a0..703e5bca753 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3240,6 +3240,8 @@ features: empty: title: Nessuna ricevuta trovata subtitle: Se stai cercando la ricevuta di un avviso pagoPA che hai pagato in passato, rivolgiti all’ente creditore. + emptyPayer: + title: Qui vedrai le ricevute dei pagamenti fatti in app details: payPal: banner: diff --git a/locales/it/index.yml b/locales/it/index.yml index 3a5cbe020ce..283a728cec9 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3240,6 +3240,8 @@ features: empty: title: Nessuna ricevuta trovata subtitle: Se stai cercando la ricevuta di un avviso pagoPA che hai pagato in passato, rivolgiti all’ente creditore. + emptyPayer: + title: Qui vedrai le ricevute dei pagamenti fatti in app details: payPal: banner: diff --git a/ts/features/payments/receipts/screens/ReceiptListScreen.tsx b/ts/features/payments/receipts/screens/ReceiptListScreen.tsx index 95d1f2d51f8..789b1386e7c 100644 --- a/ts/features/payments/receipts/screens/ReceiptListScreen.tsx +++ b/ts/features/payments/receipts/screens/ReceiptListScreen.tsx @@ -12,31 +12,39 @@ import Animated, { useSharedValue } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { PaymentsReceiptParamsList } from "../navigation/params"; -import { getPaymentsReceiptAction } from "../store/actions"; -import { walletReceiptListPotSelector } from "../store/selectors"; -import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { isPaymentsTransactionsEmptySelector } from "../../home/store/selectors"; -import { ReceiptListItemTransaction } from "../components/ReceiptListItemTransaction"; +import { NoticeListItem } from "../../../../../definitions/pagopa/biz-events/NoticeListItem"; +import { + OperationResultScreenContent, + OperationResultScreenContentProps +} from "../../../../components/screens/OperationResultScreenContent"; import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; -import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; -import { groupTransactionsByMonth } from "../utils"; import I18n from "../../../../i18n"; -import { PaymentsReceiptRoutes } from "../navigation/routes"; -import { NoticeListItem } from "../../../../../definitions/pagopa/biz-events/NoticeListItem"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; +import { isPaymentsTransactionsEmptySelector } from "../../home/store/selectors"; import * as analytics from "../analytics"; -import { ReceiptsCategoryFilter } from "../types"; -import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; import { ReceiptFadeInOutAnimationView } from "../components/ReceiptFadeInOutAnimationView"; +import { ReceiptListItemTransaction } from "../components/ReceiptListItemTransaction"; import { ReceiptLoadingList } from "../components/ReceiptLoadingList"; import { ReceiptSectionListHeader } from "../components/ReceiptSectionListHeader"; +import { PaymentsReceiptParamsList } from "../navigation/params"; +import { PaymentsReceiptRoutes } from "../navigation/routes"; +import { getPaymentsReceiptAction } from "../store/actions"; +import { walletReceiptListPotSelector } from "../store/selectors"; +import { ReceiptsCategoryFilter } from "../types"; +import { groupTransactionsByMonth } from "../utils"; export type ReceiptListScreenProps = RouteProp< PaymentsReceiptParamsList, "PAYMENT_RECEIPT_DETAILS" >; +type OperationResultEmptyProps = Pick< + OperationResultScreenContentProps, + "title" | "subtitle" | "pictogram" +>; + const AnimatedSectionList = Animated.createAnimatedComponent( SectionList as new () => SectionList ); @@ -59,7 +67,6 @@ const ReceiptListScreen = () => { const transactionsPot = useIOSelector(walletReceiptListPotSelector); const isEmpty = useIOSelector(isPaymentsTransactionsEmptySelector); - const isLoading = pot.isLoading(transactionsPot); const handleNavigateToTransactionDetails = (transaction: NoticeListItem) => { @@ -163,14 +170,23 @@ const ReceiptListScreen = () => { ); + const emptyProps: OperationResultEmptyProps = + noticeCategory === "payer" + ? { + title: I18n.t("features.payments.transactions.list.emptyPayer.title"), + pictogram: "empty" + } + : { + title: I18n.t("features.payments.transactions.list.empty.title"), + subtitle: I18n.t( + "features.payments.transactions.list.empty.subtitle" + ), + pictogram: "emptyArchive" + }; + const EmptyStateList = isEmpty ? ( - + ) : undefined; From 80d7bd563c8810002384c28a4677de923403bbd2 Mon Sep 17 00:00:00 2001 From: Daniel Hinterlechner <43072243+dhinterlechner@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:26:45 +0100 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=87=A9=F0=9F=87=AA=20updates=20(#6578?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/de/index.yml | 611 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 576 insertions(+), 35 deletions(-) diff --git a/locales/de/index.yml b/locales/de/index.yml index b21a5e836c7..8a179fe8536 100644 --- a/locales/de/index.yml +++ b/locales/de/index.yml @@ -58,6 +58,7 @@ date: - "am" - "pm" global: + why: "Warum?" you: "DU" id: "ID" badges: @@ -1090,7 +1091,7 @@ payment: IUV: "IUV" IUV_extended: "Einheitlicher Zahlungskodex (IUV)" notice: "Zahlungsmitteilung" - noticeCode: "Zahlungskodex" + noticeCode: "Zahlungsmitteilungskodex" recipientFiscalCode: "Steuernummer des Empfängers" paidConfirm: "Du hast {{amount}} gezahlt" details: @@ -1554,7 +1555,7 @@ wallet: title: "Was möchtest du bezahlen?" info: "Gib die Daten ein, die auf der Zahlungsmitteilung angegeben sind." link: "Wo finde ich die Daten?" - noticeCode: "Zahlungskodex" + noticeCode: "Zahlungsmitteilungskodex" entityCode: "Steuernummer der Körperschaft" amount: "Betrag (€)" proceed: "Mit der Transaktion fortfahren" @@ -1678,7 +1679,7 @@ wallet: chooser: "Bild aus der Galerie wählen" wrongQrCode: "QR-Code nicht gültig. Gib die Daten manuell ein." byCameraTitle: "QR-Code scannen" - cameraUsageInfo: "Scanne den QR-Code der Zahlungsmitteilung oder gib die Daten (Zahlungskodex und Steuernummer Körperschaft) manuell ein" + cameraUsageInfo: "Scanne den QR-Code der Zahlungsmitteilung oder gib die Daten (Zahlungsmitteilungskodex und Steuernummer Körperschaft) manuell ein" setManually: "Manuell eingeben" enroll_cta: "Um den QR-Code auf der Zahlungsmitteilung zu scannen, musst du IO zunächst erlauben, die Kamera zu verwenden. Du kannst dies über das Menü 'Einstellungen' deines Geräts tun." readStorageDisclosure: @@ -1794,12 +1795,14 @@ wallet: title: "Es wurden mehrere Codes entdeckt. Welchen möchtest du verwenden?" manual: noticeNumber: - title: "Gib den Zahlungskodex ein" + title: "Gib den Zahlungsmitteilungskodex ein" subtitle: "Er hat 18 Ziffern und ist neben dem QR-Code zu finden." - placeholder: "Zahlungskodex" + validationError: "18 Ziffern, die mit 0, 1 oder 3 beginnen." + placeholder: "Zahlungsmitteilungskodex" fiscalCode: title: "Gib die Steuernummer der Körperschaft ein" subtitle: "Sie hat 11 Ziffern und ist neben dem QR-Code zu finden." + validationError: "11 Ziffern eingeben." placeholder: "Steuernummer der Körperschaft" abortDialog: title: "Möchtest du den Vorgang abbrechen?" @@ -1907,6 +1910,40 @@ wallet: METHOD_NOT_ENABLED: title: "Zahlungsmethode in der App aktivieren" subtitle: "Bevor du die Zahlung erneut versuchst, wähle die Zahlungsmethode in deinem Konto aus und aktiviere die Funktion 'In-App-Zahlungen'." + PAYMENT_METHODS_NOT_AVAILABLE: + title: "Möchtest du mit Karte bezahlen? Füge sie jetzt hinzu!" + subtitle: "Die Methode wird im Konto gespeichert, so dass du beim nächsten Mal einfacher bezahlen kannst." + primaryAction: "Karte hinzufügen" + secondaryAction: "Andere Methoden verwenden" + PAYMENT_REVERSED: + title: "Die Zahlung ist fehlgeschlagen" + subtitle: "Der autorisierte Betrag wurde auf deine Zahlungsmethode zurücküberwiesen. Die Überweisung kann einige Zeit dauern." + primaryAction: "Mehr erfahren" + secondaryAction: "Schließen" + PAYPAL_REMOVED_ERROR: + title: "Autorisierung abgelehnt" + subtitle: "Möglicherweise hast du pagoPA von automatischen PayPal-Zahlungen ausgeschlossen. Lösche das PayPal-Konto aus deinem Konto, füge es wieder hinzu und versuche erneut, die Zahlung durchzuführen." + IN_APP_BROWSER_CLOSED_BY_USER: + title: "Du hast die Zahlung abgebrochen" + subtitle: "Überprüfe das Ergebnis im Tab Zahlungen. Wenn du eine Zahlung vornehmen möchtest, warte bitte mindestens 15 Minuten, bevor du es erneut versuchst." + INSUFFICIENT_AVAILABILITY_ERROR: + title: "Dein Guthaben auf der Karte ist nicht ausreichend." + subtitle: "Es wurde kein Betrag abgebucht. Bevor du es erneut versuchst, lade die Karte auf oder verwende eine andere Zahlungsmethode." + CVV_ERROR: + title: "Der eingegebene Sicherheitscode ist falsch" + subtitle: "Es wurde kein Betrag abgebucht. Der Code (CVV oder CVC) ist 3-stellig und befindet sich auf der Rückseite der Karte. Bei American Express hat der Code (CID) 4 Ziffern und befindet sich auf der Vorderseite." + PLAFOND_LIMIT_ERROR: + title: "Du hast das Ausgabenlimit deiner Karte überschritten" + subtitle: "Es wurde kein Betrag abgebucht. Bevor du es erneut versuchst, ändere das Ausgabenlimit oder verwende eine andere Zahlungsmethode." + BE_NODE_KO: + title: "Wir haben Probleme mit den Zahlungssystemen" + subtitle: "Überprüfe das Ergebnis der Zahlung im Tab Zahlungen, andernfalls warte einige Minuten, bevor du es erneut versuchst." + PSP_ERROR: + title: "Die Zahlung ist fehlgeschlagen" + subtitle: "Es wurde kein Betrag abgebucht. Sollte das Problem weiterhin bestehen, versuche es mit einer anderen Methode oder einem anderen Zahlungsanbieter." + AUTH_REQUEST_ERROR: + title: "Wir haben Probleme mit den Zahlungssystemen" + subtitle: "Versuche es später erneut." support: button: "Support kontaktieren" supportTitle: "Support kontaktieren" @@ -1915,7 +1952,7 @@ wallet: additionalDataTitle: "Zusätzliche Daten" copyAll: "Alles kopieren" errorCode: "Fehlercode" - noticeNumber: "Zahlungskodex" + noticeNumber: "Zahlungsmitteilungskodex" entityCode: "Steuernummer Körperschaft" saveCard: saveCard: "Karte speichern" @@ -1946,6 +1983,7 @@ messages: description: "{{newMessage}}, erhalten von {{organizationName}}, {{serviceName}}. {{subject}}. {{receivedAt}}. {{state}}" received_on: "erhalten am" received_at: "erhalten um" + selected: "ausgewählt" contextualHelpTitle: "Mitteilungen verwenden" counting: one: "Du hast eine Mitteilung" @@ -1990,6 +2028,8 @@ messages: reminderRemoveFailure: "Fehler beim Entfernen der Erinnerung, bitte versuche es erneut" preferenceCalendarSelect: "Wähle deinen Standardkalender" calendarPermDenied: + title: "Zugriff zulassen" + description: "Wir haben keinen Zugriff auf deinen Kalender. Bitte aktiviere die Berechtigungen, um fortzufahren." ok: "Berechtigungen freigeben" cancel: "Abbrechen" errors: @@ -2012,9 +2052,19 @@ messages: archive: success: "Erfolgreich archiviert!" failure: "Die Archivierung ist fehlgeschlagen" + generic: + failure: "Der Vorgang ist fehlgeschlagen" + success: "Vorgang erfolgreich!" restore: success: "Wiederherstellung durchgeführt." failure: "Die Wiederherstellung ist fehlgeschlagen" + search: + emptyState: + title: "Mindestens drei Zeichen eingeben" + input: + cancel: "Abbrechen" + clear: "Löschen" + placeholderShort: "Text eingeben" messageDetails: accessibilityAttachmentIcon: "Enthält Anhänge" contextualHelpTitle: "Mitteilungsdetails" @@ -2052,6 +2102,11 @@ messageDetails: bottomSheet: title: "Was ist eine dynamische Mitteilung?" body: "Die versendende Körperschaft kann den Inhalt dieser Nachricht auch nach dem Versand aktualisieren, um sicherzustellen, dass die Informationen stets korrekt und relevant sind.\n\n Dies geschieht in bestimmten Fällen, wie z. B. bei der Aktualisierung veralteter Informationen oder nicht mehr gültiger Anhänge.\n\n Wenn die Körperschaft dich über neue Informationen oder wichtige Aktualisierungen informieren muss, wird sie dies mittels einer neuen Mitteilung tun." + bodyPt1: "Es handelt sich um eine Mitteilung, die die Körperschaft " + bodyPt2: "nach dem Senden ändern kann" + bodyPt3: ". Auf diese Weise sind die darin enthaltenen Informationen immer korrekt.\nEine Körperschaft kann die Informationen in einer dynamischen Mitteilung " + bodyPt4: "nur in bestimmten Fällen" + bodyPt5: " ändern, zum Beispiel um alte Informationen zu ändern oder eine nicht mehr gültige Anlage zu aktualisieren.\nWenn sie dir neue Informationen oder wichtige Aktualisierungen mitteilen muss, schickt sie dir eine neue Mitteilung." footer: contacts: "Absender kontaktieren" showMoreData: "Mehr anzeigen" @@ -2060,8 +2115,8 @@ messageDetails: messageId: "MitteilungsID" messageIdAccessibility: "MitteilungsID kopieren" pagoPAHeader: "pagoPA Zahlungsmitteilung" - noticeCode: "Zahlungskodex" - noticeCodeAccessibility: "Zahlungskodex, kopieren" + noticeCode: "Zahlungsmitteilungskodex" + noticeCodeAccessibility: "Zahlungsmitteilungskodex, kopieren" entityFiscalCode: "Steuernummer Körperschaft" entityFiscalCodeAccessibility: "Steuernummer Körperschaft, kopieren" contactsBottomSheet: @@ -2089,9 +2144,17 @@ messagePDFPreview: opening: "Beim Öffnen des Dokuments ist ein Fehler aufgetreten. Möglicherweise hast du keine PDF-App installiert." saving: "Beim Speichern des Dokuments ist ein Fehler aufgetreten." notifications: + profileBanner: + title: "Aktiviere Push-Benachrichtigungen, damit du weißt, wenn du eine Mitteilung auf IO erhältst." + cta: "Push-Benachrichtigungen aktivieren" installation: errorTitle: "Push-Benachrichtigungen" errorMessage: "Bei der Registrierung des Push-Benachrichtigungsdienstes ist ein Fehler unterlaufen" + modal: + content: "Aktiviere Push-Benachrichtigungen, um sofort zu erfahren, wenn du eine neue Mitteilung in der App erhältst." + primaryButton: "Push-Benachrichtigungen aktivieren" + secondaryButton: "Jetzt nicht" + title: "Verpasse keine wichtigen Mitteilungen!" biometric_recognition: contextualHelpTitle: "So funktioniert die biometrische Erkennung" switchLabel: "Biometrische Erkennung aktiveren" @@ -2100,6 +2163,10 @@ biometric_recognition: needed_to_disable: "Bestätigung zur Deaktivierung erforderlich" send_email_messages: title: "Mitteilungen mittels E-Mail weiterleiten" + subtitle: "Möchtest du, dass IO eine Vorschau der Mitteilung an die E-Mail-Adresse sendet:" + switch: + title: "Eine Vorschau per E-Mail senden" + subtitle: "Du erhältst eine Vorschau der Mitteilung an deine E-Mail-Adresse, sofern der Dienst dies zulässt." services: optIn: preferences: @@ -2150,7 +2217,7 @@ services: title: "Hervorgehobene Körperschaften" institutions: title: "National" - searchLink: "Suche nach allen verfügbaren lokalen Diensten" + searchLink: "Suche nach allen verfügbaren Diensten" institution: failure: title: "Es liegt ein vorübergehendes Problem vor, bitte versuche es erneut" @@ -2209,6 +2276,7 @@ identification: congiunction: "oder" title: "Hallo {{name}}!" titleProfileName: "Hallo {{profileName}}!" + titleValidation: "Vorgang genehmigen" logout: "Abmelden" logoutProfileName: "Du bist nicht {{profileName}}?" logoutDescription: "Du kannst dich mit deinem SPID oder deiner CIE anmelden. Diese Sitzung wird abgemeldet." @@ -2253,6 +2321,7 @@ openMaps: genericError: "Ein Fehler ist aufgetreten" titleUpdateApp: "Aktualisiere IO!" msgErrorUpdateApp: "Beim Öffnen des App-Stores ist ein Fehler aufgetreten" +messageUpdateApp: "IO führt häufig kleine Verbesserungen und neue Funktionen ein: Um die App weiterhin nutzen zu können, ist es notwendig, sie auf die neueste Version zu aktualisieren." titleUpdateAppAlert: "Um fortzufahren, aktualisiere IO" messageUpdateAppAlert: "Es ist eine neue Version von IO auf {{storeName}} verfügbar, aktualisiere sie, um diese Funktion zu nutzen." openStore: "Öffne {{storeName}}" @@ -2299,20 +2368,37 @@ bonus: code: "Durch Tippen auf das Elements wird der Code kopiert" name: "Nationale Jugendkarte" departmentName: "Präsidium des italienischen Ministerrates, Abteilung für Jugendpolitik und Zivildienst" + merchantSearch: + goToSearchAccessibilityLabel: "Zur Suche der Teilnehmer gehen" + emptyList: + shortQuery: + titleWithCount: "Suche unter {{merchantCount}} Partnern der Nationalen Jugendkarte" + titleWithoutCount: "Suche bei den Partnern der Nationalen Jugendkarte" + noResults: + title: "Wir haben nichts gefunden" + subtitle: "Versuche, mit anderen Begriffen zu suchen.\nWenn du nicht fündig wirst, ist der Partner möglicherweise nicht beigetreten oder hat keine aktiven Angebote." + input: + placeholder: "Suchen" + cancel: "Abbrechen" + clear: "Löschen" merchantsList: news: "Neu" online: "Online" places: "Orte" navigationTitle: "Rabatte und Ermäßigungen" - screenTitle: Scopri le opportunità - merchantsAll: Tutti i partner + screenTitle: "Alle Vorteile entdecken" + merchantsAll: "Alle Partner" tabs: - perInitiative: Per categoria - perMerchant: Per partner + perInitiative: "Nach Kategorie" + perMerchant: "Nach Partner" cta: filter: "Filtern" categoriesList: title: "Wähle eine Kategorie und entdecke Rabatte und Ermäßigungen" + bottomSheet: + cta: "Wie sind die Kategorien angeordnet?" + title: "Wie sind die Kategorien angeordnet?" + content: "Um deine Suche zu erleichtern und deine Erfahrung zu verbessern, haben wir die Kategorien nach der Häufigkeit der Nutzung geordnet. Diese Informationen sind nicht mit deinem Profil verknüpft, sondern beruhen auf den Gesamtdaten aller Nutzer, die die Nationale Jugendkarte nutzen und der Verarbeitung ihrer Daten zugestimmt haben." filter: title: "Anbieter filtern" searchTitle: "Suche nach Name" @@ -2334,11 +2420,11 @@ bonus: byName: "Alphabetisch geordnet" merchantDetail: title: - deals: Opportunità + deals: "Vorteil" description: "Beschreibung" contactInfo: "Adressen" cta: - website: Vai al sito del partner + website: "Geh zur Webseite des Partners" categories: counting: "und weitere {{count}}" cultureAndEntertainment: "Kultur und Freizeit" @@ -2351,20 +2437,39 @@ bonus: travel: "Reisen und Transport" mobility: "Nachhaltige Mobilität" job: "Jobs und Ausbildungsplätze" + information: + address: "Adresse" + allNational: "Alle landesweiten Verkaufsstellen" + discount: + description: "Beschreibung" + conditions: "Bedingungen" + validity: "Gültigkeit" + cta: + api: "Rabattcode aktivieren" + static: "Zum Rabattcode" + bucket: "Zum Rabattcode" + landingpage: "Zum Rabatt gehen" + createNew: "Neuer Rabattcode" + secondaryCta: "Zum Rabatt gehen" + title: "Hier ist der Rabattcode!" + expired: "Der Code ist abgelaufen" + copyButton: "Rabattcode kopieren" + error: "Zurzeit ist es nicht möglich, einen Code zu generieren." cta: - activeBonus: Attiva Carta Giovani Nazionale - back: Non ora + activeBonus: "Nationale Jugendkarte aktivieren" + back: "Jetzt nicht" deactivateBonus: "Aus dem Konto entfernen" - goToDetail: Usa la Carta + goToDetail: "Karte benützen" detail: cta: - buyers: Scopri le opportunità CGN + buyers: "CGN-Vorteile entdecken" + discover: "Vorteile entdecken" otp: "Code generieren" eyca: copy: "EYCA Kartennummer kopieren" - pending: Scopri le opportunità EYCA + pending: "EYCA-Vorteile entdecken" bottomSheet: "Besuche die EYCA Website" - showEycaDiscounts: Scopri le opportunità EYCA + showEycaDiscounts: "EYCA-Vorteile entdecken" information: active: "Die Karte ist aktiv und kann bis zum {{date}} verwendet werden." warning: "Achtung! " @@ -2378,19 +2483,20 @@ bonus: eycaNumber: "Kartennummer" eycaPending: "Wir verknüpfen deine nationale Jugendkarte mit einer EYCA Nummer." eycaError: "Wir hatten Probleme mit den EYCA Systemen." - eycaBottomSheetTitle: "Cos’è il circuito EYCA?" - eycaDescription: "Fino al compimento dei 31 anni, **la tua Carta Giovani Nazionale aderisce al circuito EYCA** (European Youth Card Association).\n\nPuoi usare la Carta presso i partner aderenti, per usufruire delle opportunità disponibili per attività culturali, negozi, trasporti, ristorazione e alloggio anche nei paesi europei aderenti al circuito." + eycaBottomSheetTitle: "Was ist EYCA?" + eycaDescription: "Bis zum Alter von 31 Jahren ist **deine Nationale Jugendkarte Mitglied der EYCA** (European Youth Card Association).\nDu kannst die Karte bei den teilnehmenden Partnern verwenden, um die Vorteile für kulturelle Aktivitäten, Einkaufen, Transport, Verpflegung und Unterkunft auch in den europäischen Ländern, die an dem Programm teilnehmen, zu nutzen." badge: active: "Aktiv" revoked: "Widerrufen" expired: "Abgelaufen" date: + valid_until: "Gültig bis {{date}}" activated: "Aktiviert am" expired: "Abgelaufen am" revoked: "Widerrufen am" expiration: - cgn: "Aktiv bis" - eyca: "Aktiv bis" + cgn: "Gültig bis" + eyca: "Gültig bis" activation: eyca: loading: @@ -2401,19 +2507,19 @@ bonus: body: "Wir entschuldigen uns für die Unannehmlichkeiten.\nBitte versuche es später noch einmal." loading: caption: "Wir aktivieren deine Nationale Jugendkarte" - subCaption: "Attendi qualche secondo..." + subCaption: "Bitte warte ein paar Sekunden..." error: title: "Der Dienst Nationale Jugendkarte ist derzeit nicht verfügbar." - body: "Ci dispiace, riprova più tardi." + body: "Es tut uns leid, bitte versuche es später noch einmal." ineligible: title: "Leider erfüllst du nicht die Voraussetzungen für die Nationale Jugendkarte." body: "Die Nationale Jugendkarte ist nur für italienische und europäische Staatsbürger mit Wohnsitz in Italien im Alter von 18 bis 35 Jahren erhältlich." timeout: - title: "Stiamo lavorando la tua richiesta." + title: "Wir bearbeiten deine Anfrage." body: "Wir werden dir eine Nachricht in der App schicken, wenn deine Jugendkarte aktiv ist." alreadyActive: title: "Deine Nationale Jugendkarte ist bereits aktiv!" - body: "Puoi trovare la tua Carta nella sezione Portafoglio." + body: "Du findest sie im Tab Konto der App." pending: title: "Deine Nationale Jugendkarte wird gerade aktiviert." body: "Du erhältst eine Nachricht, wenn sie aktiviert ist." @@ -2424,8 +2530,8 @@ bonus: toast: "Zahlungsmethode von deinem Konto entfernt!" alert: title: "Karte deaktivieren" - message: "La Carta verrà disattivata e non potrai più accedere alle opportunità disponibili." - expired: Rimuovi dal Portafoglio + message: "Die Karte wird deaktiviert und du hast keinen Zugang mehr zu den verfügbaren Vorteilen." + expired: "Vom Konto entfernen" otp: error: "Aufgrund eines technischen Problems war es uns nicht möglich, einen Rabattcode zu generieren. Bitte versuche es erneut." code: @@ -2439,6 +2545,16 @@ bonus: other: "{{seconds}} Sekunden" features: messages: + pushNotifications: + banner: + title: "Verpasse keine wichtigen Mitteilungen" + body: "Aktiviere Push-Benachrichtigungen, damit du weißt, wenn du eine Mitteilung auf IO erhältst" + CTA: "Push-Benachrichtigungen aktivieren" + bottomSheet: + title: "Möchtest du diesen Hinweis nicht mehr sehen?" + body: "Bitte beachte, dass die Aktivierung von Push-Benachrichtigungen dazu beitragen kann, dass du wichtige Mitteilungen und Zahlungsfristen nicht verpasst." + cta: "Später erinnern" + cta2: "Nicht erneut anzeigen" attachmentDownloadFeedback: "Download läuft" attachments: "Anhänge" loading: @@ -2455,8 +2571,11 @@ features: content: "Läuft am {{date}} um {{time}} ab" payments: title: "pagoPA-Zahlungsmitteilungen" - noticeCode: "Zahlungskodex" + noticeCode: "Zahlungsmitteilungskodex" pay: "Zahlen" + greenPass: + button: "Zurück" + title: "Dieser Dienst ist auf IO nicht mehr aktiv" pn: service: activate: "Aktiviere den Dienst" @@ -2468,13 +2587,14 @@ features: badge: legalValue: "Rechtsgültig" title: "Details der Mitteilung" - noticeCode: "Zahlungskodex" + noticeCode: "Zahlungsmitteilungskodex" loadError: title: "Etwas ist schief gelaufen" body: "Die Details zu deiner Mitteilung konnten nicht abgerufen werden. Bitte versuche es erneut" cancelledMessage: body: "Diese Zustellung wurde vom Absender gelöscht. Du kannst dessen Inhalt ignorieren." unpaidPayments: "Für die Erstattung von Zahlungen im Zusammenhang mit diesem Bescheid wende dich bitte an den Absender." + unpaidPaymentsNew: "Für die Erstattung von Zahlungen wende dich bitte an die Körperschaft." payments: "Zahlungen" paymentSection: title: "pagoPA-Zahlungsmitteilungen" @@ -2522,6 +2642,11 @@ features: title: "Möchtest du eine Aufforderung bezahlen?" action: "Geh zum Bereich Zahlungen" close: "Schließen" + otherMethods: + error: + banner: + label: "Wir konnten einige Elemente der Liste nicht laden." + cta: "Erneut versuchen" cards: categories: all: "Alle" @@ -2532,11 +2657,24 @@ features: other: "Mehr" onboarding: title: "Was möchtest du deinem Konto hinzufügen?" + sections: + itw: "Dokumente" + other: "Mehr" + badge: + active: "Bereits vorhanden" + unavailable: "Nicht verfügbar" + requested: "In Bearbeitung" options: cgn: "Nationale Jugendkarte" welfare: "Initiativen für das Gemeinwohl" payments: "Zahlungsmethoden" payments: + backoff: + second: "1 Sekunde" + seconds: "{{seconds}} Sekunden" + minute: "1 Minute" + minutes: "{{minutes}} Minuten" + retryCountDown: "Du kannst es in {{time}} erneut versuchen." title: "Zahlungen" cta: "Aufforderung bezahlen" remoteAlert: @@ -2549,6 +2687,10 @@ features: action: "Zahlungsmethode hinzufügen" status: expired: "Abgelaufen" + error: + banner: + label: "Das Laden der Zahlungsmethoden ist fehlgeschlagen." + retryButton: "Erneut versuchen" transactions: multiplePayment: "Mehrfache Zahlung" title: "Transaktionsverlauf" @@ -2565,17 +2707,32 @@ features: shareButton: "Speichern oder teilen" error: "Die Quittung konnte nicht abgerufen werden." download: "Quittung abrufen" - hideFromList: Von der Liste verstecken + hideFromList: "Von der Liste verstecken" delete: successful: "Die Quittung wurde ausgeblendet" failed: "Es ist ein Fehler aufgetreten, bitte versuche es erneut" hideBanner: title: "Möchtest du diese Quittung aus deiner Liste ausblenden?" content: "Dieser Vorgang ist unwiderruflich. Die Quittung wird nicht mehr in deiner Quittungsliste angezeigt." - accept: "Ausblenden" + accept: "Ja, ausblenden" details: totalFeeUnknown: "Der Gesamtbetrag enthält keine Provisionskosten: du findest diese in dem Dokument, das du von {{pspName}} erhalten hast." totalFeeUnknownPsp: "Die Gesamtsumme enthält keine Provisionskosten: du findest diese in dem Dokument, das du vom Zahlungsdienstleister (PSP) erhalten hast." + error: + banner: + label: "Das Laden der Qittungen ist fehlgeschlagen." + retryButton: "Erneut versuchen" + filters: + tabs: + all: "Alle" + payer: "In-App-Zahlung" + debtor: "Auf mich lautend" + list: + empty: + title: "Keine Quittung gefunden" + subtitle: "Wenn du eine Quittung für eine pagoPA-Zahlungsmitteilung suchst, die du in der Vergangenheit bezahlt hast, wende dich an den Gläubiger." + emptyPayer: + title: "Hier findest du Quittungen für Zahlungen, die mit der App getätigt wurden." details: payPal: banner: @@ -2588,6 +2745,323 @@ features: explainationContent: "Wenn du diese Methode gespeichert hast, hast du {{pspBusinessName}} autorisiert, Transaktionen über 'PayPal-Schnellzahlung' abzuwickeln.\n\nWenn du einen anderen Anbieter verwenden möchtest, füge die Methode erneut zu deinem Konto hinzu.\n\nUm die Autorisierung zu widerrufen, entferne die Methode aus deinem Konto.\n\nVerwende die PayPal-App, um die Karte oder das Bankkonto zu ändern, von der/dem du die Gebühren abbuchen möchtest." errors: transactionCreationError: "Es gibt ein Problem mit den Bezahlsystemen." + checkout: + bottomSheet: + PAYMENT_REVERSED: + title: "Was tun, wenn die Zahlung fehlschlägt?" + payNotice: "**Bezahle die Zahlungsmitteilung**\nDenke daran, die Zahlungsmitteilung innerhalb der von der Körperschaft gesetzten Fristen zu bezahlen. Wenn du nicht über IO zahlen kannst, [entdecke andere pagoPA-fähige Kanäle]({{url}})" + waitRefund: "**Warte auf die Erstattung**\nNormalerweise wird der Betrag innerhalb weniger Minuten wieder gutgeschrieben. In anderen Fällen kann die Geldüberweisung auf dein Konto oder deine Karte länger dauern." + contactSupport: "**Kontaktiere den Support**\nWenn du nach 5 Werktagen immer noch keine Rückerstattung erhalten hast, kontaktiere bitte den Support." + itWallet: + credentialName: + eid: "Digitale Identität" + mdl: "Führerschein" + dc: "Europäische Behindertenkarte" + ts: "Gesundheitskarte - Europäische Krankenversicherungskarte" + ipzsPrivacy: + title: "Deine Dokumente in IO sind sicher" + warning: "Indem du auf **Weiter** tippst, erklärst du, dass du den **Datenschutzhinweis** gelesen und verstanden hast." + button: + label: "Weiter" + wallet: + active: "Aktiv" + inactive: "Nicht aktiv" + card: + status: + expired: "Abgelaufen" + expiring: "Ablaufend" + pending: "In Bearbeitung" + invalid: "Ungültig" + verificationExpiring: "Überprüfen" + verificationExpired: "Überprüfen" + digital: "Digitale Version" + generic: + error: + title: "Es gibt ein Problem mit unseren Systemen" + body: "Versuche es in ein paar Minuten erneut. Wenn das Problem erneut auftritt, wende dich an den Support." + alert: + title: "Möchtest du den Vorgang abbrechen?" + body: "Du musst alle Schritte erneut durchführen" + confirm: "Ja, unterbrechen" + cancel: "Nein, fortfahren" + dataSource: + multi: "Bereitgestellt von {{credentialSource}}" + single: "Bereitgestellt von {{credentialSource}}" + placeholders: + claimNotAvailable: "Eigenschaft nicht erkannt" + claimLabelNotAvailable: "Eigenschaft nicht vorhanden" + organizationName: "Körperschaft nicht verfügbar" + verifiableCredentials: + claims: + uniqueId: "Eindeutige ID" + givenName: "Name" + familyName: "Nachname" + taxIdCode: "Steuernummer" + birthdate: "Geburtsdatum" + placeOfBirth: "Geburtsort" + expirationDate: "Ablaufdatum" + securityLevel: "Sicherheitsstufe" + info: "Mehr über diese Daten" + releasedBy: "Ausstellung der digitalen Version" + attachments: "Anhänge" + authenticSource: "Quelle der Daten" + mdl: + category: "Führerschein {{category}}" + issuedDate: "Gültig von" + expirationDate: "Gültig bis" + restrictionConditions: "Beschränkungen" + discovery: + banner: + home: + title: "Neu: Dokumente in IO" + content: "Du kannst jetzt die digitale Version deiner Dokumente in dein IO-Konto aufnehmen!" + action: "Start" + onboarding: + content: "Aktiviere Dokumente in IO, um die digitale Version deiner Dokumente zum Konto hinzuzufügen" + action: "Start" + title: "Die digitale Version deiner Dokumente in IO" + content: "###### Dokumente in IO: So funktioniert es \n Du kannst jetzt die **digitale Version deiner persönlichen Dokumente**, wie z. B. deinen Führerschein und deine Gesundheitskarte, zu deinem IO-Konto hinzufügen.\n\nAktiviere die Funktion **Dokumente in IO**, um sie immer auf deinem Gerät zur Hand zu haben. \n ###### Es geht schnell und einfach \n Du benötigst deine **SPID** oder **CIE** (Elektronische Identitätskarte) Zugangsdaten, um die Aktivierung abzuschließen: dies ist ein notwendiger Sicherheitsschritt, um die Sicherheit deiner Daten zu gewährleisten." + tos: "Indem du auf **Weiter** tippst, bestätigst du, dass du die [Datenschutzbestimmungen und Nutzungsbedingungen]({{privacyAndTosUrl}}) gelesen und verstanden hast." + upcomingWalletBanner: + title: "Demnächst: deine Dokumente in IO" + content: "Bald kannst du digitale Versionen deiner persönlichen Dokumente wie Führerschein und Gesundheitskarte in dein IO-Konto einfügen!" + action: "Mehr erfahren" + alreadyActive: + title: "Dokumente in IO ist bereits aktiv" + content: "Füge weiterhin digitale Versionen deiner Dokumente zu deinem Konto hinzu." + action: "Gehe zum Konto" + identification: + mode: + title: "Überprüfe deine Identität" + description: "Dies ist ein notwendiger Schritt, um die Sicherheit deiner Daten zu gewährleisten." + header: "Wähle, wie du dich identifizieren willst" + method: + spid: + title: "SPID" + subtitle: "Zugangsdaten und App (oder SMS) verwenden" + ciePin: + title: "CIE + PIN" + subtitle: "Elektronische Identitätskarte und PIN verwenden" + cieId: + title: "CieID" + subtitle: "CieID-Zugangsdaten und -App verwenden" + nfc: + title: "NFC aktivieren, um fortzufahren" + description: "Damit IO deine CIE lesen kann, aktiviere NFC in den Einstellungen deines Geräts." + header: "Wie geht das" + steps: + label: "Schritt {{value}}" + 1: "Öffne 'Einstellungen'" + 2: "Suche nach 'NFC'" + 3: "Aktiviere die Funktion" + primaryAction: "Einstellungen öffnen" + secondaryAction: "Weiter" + notMatchingIdentityScreen: + title: "Du meldest dich mit einem neuen Gerät an" + message: "Wenn du dich von einem anderen Gerät als dem üblichen (z. B. dem einer anderen Person) in IO anmeldest, werden aus Sicherheitsgründen diese Funktionalitäten zurückgesetzt:\n\n- Dokumente in IO" + banner: + title: "Wir empfehlen, dass du nur von deinem Gerät aus auf die App zugreifst." + alert: + title: "Möchtest du dich wirklich abmelden?" + message: "Du musst dich erneut mit SPID oder CIE anmelden, um die App nutzen zu können." + loading: + cieId: + title: "Verbindung mit der CieID-App läuft..." + subtitle: "Bestätige den Zugang in der CieID-App, um die Verbindung fortzusetzen oder abzubrechen" + cancel: "Verbindung abbrechen" + issuance: + credentialAuth: + title: "{{credentialName}}: erforderliche Daten" + subtitle: "Sie werden mit **{{organization}}** für die Bereitstellung der digitalen Version des Dokuments geteilt." + requiredClaims: "Erforderliche Daten" + disclaimer: + 0: "Deine Daten sind sicher und werden nur zu den in der Datenschutzrichtlinie beschriebenen Zwecken verarbeitet." + 1: "Die Daten werden nur so lange weitergegeben, wie es dauert, bis die digitale Version des Dokuments bereitgestellt wird." + tos: "Indem du auf **Weiter** tippst, bestätigst du, dass du die [Datenschutzrichtlinie]({{privacyUrl}}) gelesen und verstanden hast." + eidPreview: + title: "Identität verifiziert" + subtitle: "Du aktivierst **Dokumente in IO** als:" + actions: + primary: "Weiter" + secondary: "Abbrechen" + credentialPreview: + loading: "Warte noch ein paar Sekunden, ohne die App zu beenden." + title: "{{credential}}: Hier eine Vorschau deiner Daten" + bottomSheet: + about: + title: "Wer ist das?" + subtitle: "Das ist die Körperschaft, die dir die digitale Version deiner Dokumente zur Verfügung stellt.\n\nUm zu erfahren, wie sie deine Daten verarbeitet, konsultiere bitte die [Datenschutzrichtlinie]({{privacyUrl}})." + authSource: + title: "Wer ist das?" + subtitle: "Es ist die Körperschaft, die die in deinem Dokument enthaltenen Daten bereitstellt." + actions: + primary: "Dem Konto hinzufügen" + secondary: "Abbrechen" + eidResult: + success: + title: "Alles ist bereit!" + subtitle: "Du kannst nun das erste Dokument in das IO-Konto aufnehmen." + toast: "Erledigt!" + actions: + continue: "Erstes Dokument hinzufügen" + continueAlt: "Dokument hinzufügen" + close: "Später fortfahren" + credentialResult: + toast: "Erledigt!" + notMatchingIdentityError: + title: "Unerkannte Identität" + body: "Aktiviere Dokumente in IO mit der gleichen Identität, die du für den Zugriff auf die IO-App verwendest. Überprüfe deine Anmeldedaten und versuche es erneut." + primaryAction: "Zugriff wiederholen" + secondaryAction: "Schließen" + genericError: + title: "Es ist ein unerwarteter Fehler aufgetreten" + body: "Die Körperschaft, die digitale Versionen von Dokumenten ausgibt, hat Probleme und arbeitet bereits an deren Lösung: Versuche es später noch einmal." + primaryAction: "Schließen" + notEntitledCredentialError: + title: "Das Dokument kann nicht hinzugefügt werden" + body: "Vergewissere dich, dass du im Besitz des gültigen physischen Dokuments bist, bevor du dessen digitale Version anforderst." + primaryAction: "Verstanden" + asyncCredentialError: + title: "Das Führerscheinamt bearbeitet deinen Antrag" + body: "Du erhältst in der App eine Mitteilung, dass du fortfahren kannst, sobald dein Antrag bearbeitet ist." + primaryAction: "Verstanden" + credentialAlreadyAdded: + title: "Du hast dieses Dokument bereits" + body: "Die digitale Version des Dokuments befindet sich bereits in deinem Konto." + primaryAction: "Gehe zum Dokument" + walletInstanceNotActive: + title: "Aktiviere Dokumente in IO, um fortzufahren" + body: "Um deine Dokumente zum Konto hinzuzufügen, musst du Folgendes aktivieren: " + bodyBold: "Dokumente in IO" + primaryAction: "Start" + secondaryAction: "Jetzt nicht" + credentialNotFound: + title: "Dokument zum Konto hinzufügen" + subtitle: "Um Dokumente in IO zu verwenden, füge sie zunächst dem Konto hinzu. Das geht schnell und einfach." + unsupportedDevice: + text: "Ab dem 02.07.2024 ist dein Gerät möglicherweise nicht mehr mit IO kompatibel." + moreInfo: "Mehr erfahren" + error: + title: "Dein Gerät unterstützt Dokumente in IO nicht" + body: "Dein Gerät verfügt nicht über die erforderlichen Sicherheitsanforderungen für diese Funktion." + primaryAction: "Schließen" + secondaryAction: "Mehr erfahren" + presentation: + alerts: + statusAction: "Was kann ich tun?" + mdl: + content: "Du kannst deinen Führerschein nur in Italien verwenden, um bei einer Polizeikontrolle deine Fahrtauglichkeit nachzuweisen." + ehc: + content: "Du kannst deine Gesundheitskarte - Europäische Krankenversicherungskarte auf IO verwenden, um Dienstleistungen des nationalen Gesundheitsdienstes in Anspruch zu nehmen." + edc: + content: "Du kannst deinen Europäischen Behindertenausweis auf IO verwenden, um Dienstleistungen auf italienischem Gebiet in Anspruch zu nehmen, und zwar in denselben Verwendungszusammenhängen wie dein physisches Dokument." + expired: + content: "Das Dokument ist nicht mehr gültig. Wenn du das neue gültige Dokument bereits hast, kannst du die digitale Version im Konto aktualisieren" + action: "Dokument aktualisieren" + expiring: + content: "Es verbleiben noch {{days}} Tage, bevor das Dokument abläuft." + verificationExpiring: + content: "Überprüfe die digitale Version deines Dokuments bis zum {{date}}." + bottomSheets: + eidInfo: + title: "Dokumente in IO:\nIdentität verifiziert" + titleExpired: "Dokumente in IO:\nVerifiziere deine Identität" + contentTop: "Mit **Dokumente in IO** speicherst du digitale Versionen deiner Dokumente im IO-Konto." + contentBottom: "###### Wie funktioniert das?\n\nDeine **Identität wird bei der Aktivierung über SPID oder CIE verifiziert**." + triggerLabel: "Was ist das?" + alert: + valid: "Die letzte Überprüfung ist vom {{date}}." + expiring: "Überprüfe deine Identität bis {{date}}." + expired: "Für die weitere Nutzung von Dokumente in IO ist ein kurzer Überprüfungsschritt erforderlich." + MDL: + expiring: + title: "Führerschein in IO: ablaufendes Dokument" + content: "Verlängere deinen Führerschein in den Büros des Ministeriums für Infrastruktur und Verkehr - Generaldirektion für Motorisierung oder bei einer autorisierten Stelle in deiner Nähe.\n\nDu musst dich einer medizinischen Untersuchung bei einem qualifizierten Arzt unterziehen und die vorgeschriebenen Formulare ausfüllen.\n\n###### Welche Dokumente sollte ich bereithalten?\n\n - den ablaufenden oder abgelaufenen Führerschein\n - ein Ausweisdokument\n - die Steuernummer\n - ein aktuelles Foto in Passgröße" + EuropeanHealthInsuranceCard: + expiring: + title: "Gesundheitskarte in IO: ablaufendes Dokument" + content: "Deine Gesundheitskarte wird automatisch erneuert und dann an deine bei der Agentur der Einnahmen registrierte Adresse geschickt.\n\nWenn du sie bereits erhalten hast, kannst du dieses Dokument aus deinem Konto löschen und die neue, aktualisierte digitale Version hinzufügen." + EuropeanDisabilityCard: + expiring: + title: "Europäischer Behindertenausweis in IO: ablaufendes Dokument" + content: "Du kannst die Verlängerung deines Europäischen Behindertenausweises bei der Stelle beantragen, die ihn ausgestellt hat.\n\n Informiere dich auf der offiziellen Website des INPS oder wende dich an deinen Sanitätsbetrieb, da die Verlängerung je nach Art der Behinderung und den Vorschriften in deinem Gebiet unterschiedlich ausfallen kann." + credentialDetails: + card: + front: "Vorderseite " + back: "Rückseite" + showFront: "Zeige Vorderseite " + showBack: "Zeige Rückseite" + personalDataTitle: "Persönliche Daten" + documentDataTitle: "Daten des Dokuments" + lastUpdated: "Daten zum {{lastUpdateTime}}" + boolClaim: + true: "Ja" + false: "Nein" + hiddenClaim: "Ausgeblendet" + fiscalCode: + label: "Deine Steuernummer" + action: "Tippe, um den Barcode zu vergrößern und im Vollbildmodus anzuzeigen." + status: + valid: "Gültig" + invalid: "Ungültig" + expired: "Abgelaufen" + expiring: "Ablaufend" + actions: + removeFromWallet: "Vom Konto entfernen" + requestAssistance: "Stimmt etwas nicht?" + showClaimValues: "Dokumentattribute anzeigen" + hideClaimValues: "Dokumentattribute ausblenden" + dialogs: + remove: + title: "Möchtest du das Dokument aus dem Konto entfernen?" + content: "Wenn du deine Meinung änderst, kannst du es später wieder hinzufügen." + confirm: "Ja, entfernen" + toast: + removed: "Erledigt!" + verificationExpired: + title: "Überprüfe die digitale Version des Dokuments" + content: "Dies ist ein notwendiger Sicherheitsschritt, um das Dokument '{{credentialName}}' in IO weiter zu verwenden." + primaryAction: "Start" + ctas: + openPdf: "Dokument anzeigen" + shareButton: "Speichern oder freigeben" + fiscalCode: "Deine Steuernummer" + trustmark: + cta: "Echtheitszertifikat anzeigen" + description: "Zeige den QR-Code vor, um die Echtheit des Dokuments zu bestätigen, wenn du dazu aufgefordert wirst." + expiration: "Der QR-Code erneuert sich in" + qrCode: "QR-Code zur Authentizität von Dokumenten" + walletRevocation: + cta: "Dokumente in IO deaktivieren" + confirmScreen: + title: "Möchtest du Dokumente in IO wirklich deaktivieren?" + subtitle: "Du löschst die Dokumente, die du dem Konto hinzugefügt hast.\nWenn du deine Meinung änderst, kannst du Dokumente in IO in Zukunft wieder aktivieren." + action: "Bestätige und fortfahren" + loadingScreen: + title: "Wir deaktivieren Dokumente in IO..." + subtitle: "Warte ein paar Sekunden" + failureScreen: + title: "Ein unerwarteter Fehler ist aufgetreten" + subtitle: "Der Dienst konnte nicht deaktiviert werden. Bitte versuche es erneut." + feedback: + banner: + title: "Sag uns, was du denkst" + content: "Erzähle uns von deinen Erfahrungen mit der Funktion Dokumente in IO." + action: "Start" + walletInstanceRevoked: + alert: + cta: "Mehr erfahren" + closeButton: "Schließen" + closeButtonAlt: "Verstanden" + revokedByWalletProvider: + title: "Dokumente in IO wurde deaktiviert" + content: "Um die Voraussetzungen für die weitere Nutzung der Funktionen auf deinem Gerät zu prüfen, tippe auf 'Mehr erfahren'." + newWalletInstanceCreated: + title: "Dokumente in IO wurde auf diesem Gerät deaktiviert" + content: "Aus Sicherheitsgründen kannst du deine IO-Dokumente immer nur auf einem Gerät gleichzeitig verwenden." + revokedByUser: + title: "Du hast Dokumente in IO deaktiviert" + content: "Wenn du deine Meinung änderst, kannst du Dokumente in IO in Zukunft wieder aktivieren." support: ticketList: noTicket: @@ -2605,6 +3079,9 @@ support: panicMode: title: "Leider können wir dir zur Zeit nicht helfen" body: "Wir wissen, dass es ein Problem gibt, und wir arbeiten daran, es zu lösen. Wenn das Problem weiterhin besteht oder wenn du in einer anderen Angelegenheit Hilfe benötigst, versuche bitte, uns später erneut zu kontaktieren." + errorGetZendeskToken: + title: "Wir können derzeit kein Ticket erstellen" + subtitle: "Versuche es später erneut" askPermissions: nameSurname: "Vor- und Nachname" fiscalCode: "Steuernummer" @@ -2720,6 +3197,8 @@ transaction: totalFee: "Der Gesamtbetrag umfasst " totalFeePsp: "Provision, berechnet von {{pspName}}." totalFeeNoPsp: "Provision, die vom Transaktionsdienstleister (PSP) erhoben wird." + bannerImported: + content: "Die pagoPA-Quittung ist nicht verfügbar. Wende dich an den Zahlungsempfängers, wenn du den Zahlungsbeleg, d. h. das Dokument, das die Begleichung einer Schuld bescheinigt, benötigst." info: title: "Informationen zur Transaktion" pspName: "Zahlungsdienstleister (PSP)" @@ -2758,3 +3237,65 @@ permissionRequest: 2: "Wähle 'IO'" 3: "Wähle 'Fotos' und erlaube den Zugriff" cta: "Öffne Einstellungen" +FIMS: + updateApp: + header: "Aktualisiere die App, um fortzufahren" + body: "Um weiterhin alle Funktionen nutzen zu können, lade die neue Version von IO aus dem Store herunter." + history: + errorStates: + dataUnavailable: "Daten nicht verfügbar" + ko: + title: "Es gibt ein vorübergehendes Problem" + body: "Hier kannst du den Verlauf deiner Zugriffe auf externe Dienste über IO einsehen und eine Kopie per E-Mail anfordern." + toast: "Beim Laden der Liste ist ein Problem aufgetreten. Bitte versuche es erneut" + emptyBody: "Du hast noch keine Dienste Dritter in Anspruch genommen" + exportData: + alerts: + areYouSure: "Willst du wirklich eine Kopie aller Zugänge exportieren?" + alreadyExporting: + title: "Wir bearbeiten bereits einen Exportantrag." + body: "Wenn die Bearbeitung abgeschlossen ist, erhältst du eine E-Mail mit allen Informationen zum Zugriffsverlauf." + CTA: "Eine Kopie per E-Mail anfordern" + successToast: "Erledigt! Prüfe dein Postfach." + errorToast: "Beim Senden der Anfrage ist ein Problem aufgetreten. Bitte versuche es erneut" + profileCTA: + title: "Kontrolle deiner Zugriffe" + subTitle: "Zugriffshistorie mit IO anzeigen" + historyScreen: + header: "Deine Zugänge auf Dienste Dritter" + body: "Hier kannst du die Historie deiner Zugänge auf externe Dienste über IO einsehen und eine Kopie per E-Mail anfordern." + loadingScreen: + abort: + title: "Du hast den Zugang zu diesem Dienst abgebrochen" + consents: + title: "Warte ein paar Sekunden" + idle: + title: "Warte ein paar Sekunden" + fastLogin_forced_restart: + title: "Warte ein paar Sekunden" + in-app-browser-loading: + title: "Wir leiten dich zum Dienst weiter" + subtitle: "Warte ein paar Sekunden" + consentsScreen: + errorStates: + authentication: + body: "Der Webservice der Körperschaft ist derzeit nicht verfügbar" + title: "Der Dienst konnte nicht erreicht werden" + general: + body: "Es gab ein Problem beim Abrufen der Daten: Wir arbeiten daran, es so schnell wie möglich zu lösen" + title: "Es liegt ein vorübergehendes Problem vor, bitte versuche es später noch einmal." + missingInAppBrowser: + body: "Um einen sicheren Zugriff zu gewährleisten, lade den auf deinem Gerät installierten Browser herunter oder aktualisiere ihn." + title: "Aktualisiere deinen Browser um fortzufahren" + inAppBrowserError: "Beim Senden der Anfrage ist ein Problem aufgetreten. Bitte versuche es erneut" + title: "Um auf den Dienst zugreifen zu können, müssen die folgenden Daten freigegeben werden" + subtitle: "IO wird die erforderlichen Daten an " + subtitle2: " weitergeben für deine Authentifizierung bei " + requiredData: "Erforderliche Daten" + privacy1: "Für weitere Informationen lies bitte die " + privacyCta: "Datenschutzrichtlinie " + bottomSheet: + title: "Dienste von Drittanbietern über IO: So funktioniert es" + body: "Damit du auf einen externen Dienst zugreifen kannst, ohne dich jedes Mal zu authentifizieren, gibt die IO-App einige deiner Daten an den Anbieter weiter." + body2: "Deine Daten sind sicher und werden nur zu den Zwecken verarbeitet, die von der Körperschaft angegeben sind." + bodyPrivacy: "Datenschutzrichtlinie"