diff --git a/locales/en/index.yml b/locales/en/index.yml index 0351bdc8b29..08f3c53c397 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3297,6 +3297,9 @@ features: claimNotAvailable: "Attributo non riconosciuto" claimLabelNotAvailable: "Etichetta attributo non presente" organizationName: "Nome ente non disponibile" + walletNotAvailable: + message: Abbiamo avuto un problema nel recuperare i tuoi documenti. + cta: Chiudi e riapri l'app per riprovare. verifiableCredentials: claims: uniqueId: "ID univoco" diff --git a/locales/it/index.yml b/locales/it/index.yml index 38adc05833c..c1fbbae8503 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3297,6 +3297,9 @@ features: claimNotAvailable: "Attributo non riconosciuto" claimLabelNotAvailable: "Etichetta attributo non presente" organizationName: "Nome ente non disponibile" + walletNotAvailable: + message: Abbiamo avuto un problema nel recuperare i tuoi documenti. + cta: Chiudi e riapri l'app per riprovare. verifiableCredentials: claims: uniqueId: "ID univoco" diff --git a/ts/features/itwallet/common/components/ItwWalletNotAvailableBanner.tsx b/ts/features/itwallet/common/components/ItwWalletNotAvailableBanner.tsx new file mode 100644 index 00000000000..04d11052a0a --- /dev/null +++ b/ts/features/itwallet/common/components/ItwWalletNotAvailableBanner.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { StyleSheet, View } from "react-native"; +import { + BodySmall, + IOAlertSpacing, + IOColors, + Icon +} from "@pagopa/io-app-design-system"; +import I18n from "../../../../i18n"; +import { useIOSelector } from "../../../../store/hooks"; +import { itwIsWalletInstanceStatusFailureSelector } from "../../walletInstance/store/reducers"; + +/** + * Component shown when it is not possible to retrieve the wallet instance status + * from the backend, because of unexpected errors. + */ +export const ItwWalletNotAvailableBanner = () => { + const isWalletInstanceStatusFailed = useIOSelector( + itwIsWalletInstanceStatusFailureSelector + ); + + if (!isWalletInstanceStatusFailed) { + return null; + } + + return ( + + + + {I18n.t("features.itWallet.generic.walletNotAvailable.message")} + + + {I18n.t("features.itWallet.generic.walletNotAvailable.cta")} + + + ); +}; + +const styles = StyleSheet.create({ + bannerContainer: { + padding: IOAlertSpacing[1], + marginVertical: 16, + backgroundColor: IOColors["grey-50"], + borderRadius: 8, + alignItems: "center", + gap: 8 + }, + textCenter: { + textAlign: "center" + } +}); diff --git a/ts/features/itwallet/common/store/selectors/index.ts b/ts/features/itwallet/common/store/selectors/index.ts index b4aa0bc14ff..53245186302 100644 --- a/ts/features/itwallet/common/store/selectors/index.ts +++ b/ts/features/itwallet/common/store/selectors/index.ts @@ -8,6 +8,7 @@ import { itwIsWalletEmptySelector } from "../../../credentials/store/selectors"; import { itwLifecycleIsValidSelector } from "../../../lifecycle/store/selectors"; +import { itwIsWalletInstanceStatusFailureSelector } from "../../../walletInstance/store/reducers"; import { itwIsFeedbackBannerHiddenSelector, itwIsDiscoveryBannerHiddenSelector @@ -49,7 +50,7 @@ export const itwShouldRenderFeedbackBannerSelector = (state: GlobalState) => /** * Returns if the wallet ready banner should be visible. The banner is visible if: - * - The Wallet has valid Wallet Instance and a valid eID + * - The Wallet has valid Wallet Instance with a known status, and a valid eID * - The eID is not expired * - The Wallet is empty * @param state the application global state @@ -57,5 +58,6 @@ export const itwShouldRenderFeedbackBannerSelector = (state: GlobalState) => */ export const itwShouldRenderWalletReadyBannerSelector = (state: GlobalState) => itwLifecycleIsValidSelector(state) && + !itwIsWalletInstanceStatusFailureSelector(state) && itwCredentialsEidStatusSelector(state) !== "jwtExpired" && itwIsWalletEmptySelector(state); diff --git a/ts/features/itwallet/common/utils/itwTypesUtils.ts b/ts/features/itwallet/common/utils/itwTypesUtils.ts index fe04304f663..8be73437e2f 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,13 @@ export type ParsedStatusAttestation = Awaited< ReturnType >["parsedStatusAttestation"]["payload"]; +/** + * Alias for the WalletInstanceStatus type + */ +export type WalletInstanceStatus = Awaited< + ReturnType +>; + export type StoredStatusAttestation = | { credentialStatus: "valid"; diff --git a/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts b/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts index 779a11a929e..b75d230c6c0 100644 --- a/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts +++ b/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts @@ -1,11 +1,13 @@ import * as O from "fp-ts/lib/Option"; import { call, put, select } from "typed-redux-saga/macro"; import { sessionTokenSelector } from "../../../../store/reducers/authentication"; +import { FAILURE_STATUS } from "../../walletInstance/store/reducers"; import { ReduxSagaEffect } from "../../../../types/utils"; import { assert } from "../../../../utils/assert"; import { getWalletInstanceStatus } from "../../common/utils/itwAttestationUtils"; import { ensureIntegrityServiceIsReady } from "../../common/utils/itwIntegrityUtils"; import { itwIntegrityKeyTagSelector } from "../../issuance/store/selectors"; +import { itwUpdateWalletInstanceStatus } from "../../walletInstance/store/actions"; import { itwLifecycleIsOperationalOrValid } from "../store/selectors"; import { itwIntegritySetServiceIsReady } from "../../issuance/store/actions"; import { handleWalletInstanceResetSaga } from "./handleWalletInstanceResetSaga"; @@ -14,14 +16,21 @@ export function* getStatusOrResetWalletInstance(integrityKeyTag: string) { const sessionToken = yield* select(sessionTokenSelector); assert(sessionToken, "Missing session token"); - const walletInstanceStatus = yield* call( - getWalletInstanceStatus, - integrityKeyTag, - sessionToken - ); + try { + const walletInstanceStatus = yield* call( + getWalletInstanceStatus, + integrityKeyTag, + sessionToken + ); + + if (walletInstanceStatus.is_revoked) { + yield* call(handleWalletInstanceResetSaga); + } - if (walletInstanceStatus.is_revoked) { - yield* call(handleWalletInstanceResetSaga); + // Update wallet instance status + yield* put(itwUpdateWalletInstanceStatus(walletInstanceStatus)); + } catch (_) { + yield* put(itwUpdateWalletInstanceStatus(FAILURE_STATUS)); } } diff --git a/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx b/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx index 0efb8a6768f..c25c95b63ca 100644 --- a/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx +++ b/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx @@ -41,6 +41,7 @@ import { } from "../../machine/credential/selectors"; import { ItwCredentialIssuanceMachineContext } from "../../machine/provider"; import { ItwOnboardingModuleCredential } from "../components/ItwOnboardingModuleCredential"; +import { itwIsWalletInstanceStatusFailureSelector } from "../../walletInstance/store/reducers"; // List of available credentials to show to the user const availableCredentials = [ @@ -57,15 +58,19 @@ const activeBadge: Badge = { const WalletCardOnboardingScreen = () => { const isItwValid = useIOSelector(itwLifecycleIsValidSelector); const isItwEnabled = useIOSelector(isItwEnabledSelector); + const isWalletInstanceStatusFailure = useIOSelector( + itwIsWalletInstanceStatusFailureSelector + ); useFocusEffect(trackShowCredentialsList); const isItwSectionVisible = React.useMemo( // IT Wallet credential catalog should be visible if () => + !isWalletInstanceStatusFailure && isItwValid && // An eID has ben obtained and wallet is valid isItwEnabled, // Remote FF is enabled - [isItwValid, isItwEnabled] + [isItwValid, isItwEnabled, isWalletInstanceStatusFailure] ); return ( diff --git a/ts/features/itwallet/walletInstance/store/actions/index.ts b/ts/features/itwallet/walletInstance/store/actions/index.ts index 3d0c05b05c2..19a6a585d0f 100644 --- a/ts/features/itwallet/walletInstance/store/actions/index.ts +++ b/ts/features/itwallet/walletInstance/store/actions/index.ts @@ -1,4 +1,6 @@ import { ActionType, createStandardAction } from "typesafe-actions"; +import { WalletInstanceStatus } from "../../../common/utils/itwTypesUtils"; +import { FailureStatus } from "../reducers"; /** * This action stores the Wallet Instance Attestation @@ -7,6 +9,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..15b351ad6b9 100644 --- a/ts/features/itwallet/walletInstance/store/reducers/index.ts +++ b/ts/features/itwallet/walletInstance/store/reducers/index.ts @@ -8,14 +8,23 @@ 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 { + itwUpdateWalletInstanceStatus, + itwWalletInstanceAttestationStore +} from "../actions"; +import { WalletInstanceStatus } from "../../../common/utils/itwTypesUtils"; + +export const FAILURE_STATUS = "failure"; +export type FailureStatus = typeof FAILURE_STATUS; export type ItwWalletInstanceState = { attestation: string | undefined; + status: WalletInstanceStatus | FailureStatus | undefined; }; export const itwWalletInstanceInitialState: ItwWalletInstanceState = { - attestation: undefined + attestation: undefined, + status: undefined }; const CURRENT_REDUX_ITW_WALLET_INSTANCE_STORE_VERSION = -1; @@ -27,10 +36,17 @@ const reducer = ( switch (action.type) { case getType(itwWalletInstanceAttestationStore): { return { + ...state, attestation: action.payload }; } + case getType(itwUpdateWalletInstanceStatus): + return { + ...state, + status: action.payload + }; + case getType(itwLifecycleStoresReset): return { ...itwWalletInstanceInitialState }; @@ -62,4 +78,11 @@ export const itwIsWalletInstanceAttestationValidSelector = createSelector( ) ); +/** + * Returns true when it was not possible to retrieve the wallet instance status, + * for instance because of unexpected errors. + */ +export const itwIsWalletInstanceStatusFailureSelector = (state: GlobalState) => + state.features.itWallet.walletInstance.status === FAILURE_STATUS; + export default persistedReducer; diff --git a/ts/features/wallet/components/WalletCardsContainer.tsx b/ts/features/wallet/components/WalletCardsContainer.tsx index 394dc54ee19..6efff374d2c 100644 --- a/ts/features/wallet/components/WalletCardsContainer.tsx +++ b/ts/features/wallet/components/WalletCardsContainer.tsx @@ -16,6 +16,7 @@ import { import { ItwEidLifecycleAlert } from "../../itwallet/common/components/ItwEidLifecycleAlert"; import { ItwFeedbackBanner } from "../../itwallet/common/components/ItwFeedbackBanner"; import { ItwWalletReadyBanner } from "../../itwallet/common/components/ItwWalletReadyBanner"; +import { ItwWalletNotAvailableBanner } from "../../itwallet/common/components/ItwWalletNotAvailableBanner"; import { itwCredentialsEidStatusSelector } from "../../itwallet/credentials/store/selectors"; import { itwLifecycleIsValidSelector } from "../../itwallet/lifecycle/store/selectors"; import { @@ -27,6 +28,7 @@ import { selectWalletOtherCards, shouldRenderWalletEmptyStateSelector } from "../store/selectors"; +import { itwIsWalletInstanceStatusFailureSelector } from "../../itwallet/walletInstance/store/reducers"; import { WalletCardCategoryFilter } from "../types"; import { WalletCardsCategoryContainer } from "./WalletCardsCategoryContainer"; import { WalletCardsCategoryRetryErrorBanner } from "./WalletCardsCategoryRetryErrorBanner"; @@ -47,6 +49,9 @@ const WalletCardsContainer = () => { const shouldRenderEmptyState = useIOSelector( shouldRenderWalletEmptyStateSelector ); + const isWalletInstanceStatusFailure = useIOSelector( + itwIsWalletInstanceStatusFailureSelector + ); // Loading state is only displayed if there is the initial loading and there are no cards or // placeholders in the wallet @@ -69,17 +74,25 @@ const WalletCardsContainer = () => { } return ( - {shouldRenderCategory("itw") && } + {!isWalletInstanceStatusFailure && shouldRenderCategory("itw") && ( + + )} {shouldRenderCategory("other") && } ); - }, [shouldRenderEmptyState, shouldRenderCategory, shouldRenderLoadingState]); + }, [ + shouldRenderEmptyState, + shouldRenderCategory, + shouldRenderLoadingState, + isWalletInstanceStatusFailure + ]); return ( + {walletContent} diff --git a/ts/features/wallet/components/WalletCategoryFilterTabs.tsx b/ts/features/wallet/components/WalletCategoryFilterTabs.tsx index 9cec1e0534e..6d4db0117da 100644 --- a/ts/features/wallet/components/WalletCategoryFilterTabs.tsx +++ b/ts/features/wallet/components/WalletCategoryFilterTabs.tsx @@ -10,8 +10,8 @@ import { useIODispatch, useIOSelector } from "../../../store/hooks"; import { trackWalletCategoryFilter } from "../../itwallet/analytics"; import { walletSetCategoryFilter } from "../store/actions/preferences"; import { - selectWalletCategories, - selectWalletCategoryFilter + selectWalletCategoryFilter, + shouldRenderCategoryFiltersSelector } from "../store/selectors"; import { walletCardCategoryFilters } from "../types"; @@ -24,7 +24,7 @@ const WalletCategoryFilterTabs = () => { const dispatch = useIODispatch(); const selectedCategory = useIOSelector(selectWalletCategoryFilter); - const categories = useIOSelector(selectWalletCategories); + const shouldRender = useIOSelector(shouldRenderCategoryFiltersSelector); const selectedIndex = React.useMemo( () => @@ -34,7 +34,7 @@ const WalletCategoryFilterTabs = () => { [selectedCategory] ); - if (categories.size <= 1) { + if (!shouldRender) { return null; } diff --git a/ts/features/wallet/components/__tests__/WalletCategoryFilterTabs.test.tsx b/ts/features/wallet/components/__tests__/WalletCategoryFilterTabs.test.tsx index 06452f7ab0c..abbb5f2824a 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 when its rendering conditions are not met", () => { jest .spyOn(selectors, "selectWalletCategoryFilter") .mockImplementation(() => undefined); jest - .spyOn(selectors, "selectWalletCategories") - .mockImplementation(() => new Set(["itw"])); + .spyOn(selectors, "shouldRenderCategoryFiltersSelector") + .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 when its rendering conditions are met", () => { jest .spyOn(selectors, "selectWalletCategoryFilter") .mockImplementation(() => undefined); jest - .spyOn(selectors, "selectWalletCategories") - .mockImplementation(() => new Set(["itw", "other"])); + .spyOn(selectors, "shouldRenderCategoryFiltersSelector") + .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, "shouldRenderCategoryFiltersSelector") + .mockImplementation(() => true); const { getByTestId } = renderComponent(); const itwTab = getByTestId("CategoryTabTestID-itw"); diff --git a/ts/features/wallet/store/selectors/__tests__/index.test.ts b/ts/features/wallet/store/selectors/__tests__/index.test.ts index bf524fdbf2d..a8833b86241 100644 --- a/ts/features/wallet/store/selectors/__tests__/index.test.ts +++ b/ts/features/wallet/store/selectors/__tests__/index.test.ts @@ -5,6 +5,7 @@ import { isWalletEmptySelector, selectWalletCards, selectWalletCategories, + shouldRenderCategoryFiltersSelector, shouldRenderWalletEmptyStateSelector } from ".."; import { applicationChangeState } from "../../../../../store/actions/application"; @@ -16,6 +17,7 @@ import { import { ItwLifecycleState } from "../../../../itwallet/lifecycle/store/reducers"; import * as itwLifecycleSelectors from "../../../../itwallet/lifecycle/store/selectors"; import { WalletCardsState } from "../../reducers/cards"; +import { FAILURE_STATUS } from "../../../../itwallet/walletInstance/store/reducers"; const T_CARDS: WalletCardsState = { "1": { @@ -220,3 +222,34 @@ describe("shouldRenderWalletEmptyStateSelector", () => { } ); }); + +describe("shouldRenderCategoryFiltersSelector", () => { + it.each` + walletCards | walletInstanceStatus | expected + ${[{ category: "itw" }]} | ${undefined} | ${false} + ${[{ category: "itw" }]} | ${FAILURE_STATUS} | ${false} + ${[{ category: "itw" }, { category: "cgn" }]} | ${undefined} | ${true} + ${[{ category: "itw" }, { category: "cgn" }]} | ${FAILURE_STATUS} | ${false} + `( + "should return $expected when walletCards are $walletCards.length and walletInstanceStatus is $walletInstanceStatus", + ({ walletCards, walletInstanceStatus, expected }) => { + const globalState = appReducer( + undefined, + applicationChangeState("active") + ); + + const shouldRenderCategoryFilters = shouldRenderCategoryFiltersSelector( + _.merge( + globalState, + _.set( + globalState, + "features.itWallet.walletInstance.status", + walletInstanceStatus + ), + _.set(globalState, "features.wallet.cards", walletCards) + ) + ); + expect(shouldRenderCategoryFilters).toBe(expected); + } + ); +}); diff --git a/ts/features/wallet/store/selectors/index.ts b/ts/features/wallet/store/selectors/index.ts index 0fc70d8a3fd..79bfe968fcf 100644 --- a/ts/features/wallet/store/selectors/index.ts +++ b/ts/features/wallet/store/selectors/index.ts @@ -7,6 +7,7 @@ import { itwLifecycleIsValidSelector } from "../../../itwallet/lifecycle/store/s import { paymentsWalletUserMethodsSelector } from "../../../payments/wallet/store/selectors"; import { WalletCard, walletCardCategories } from "../../types"; import { isSomeLoadingOrSomeUpdating } from "../../../../utils/pot"; +import { itwIsWalletInstanceStatusFailureSelector } from "../../../itwallet/walletInstance/store/reducers"; const selectWalletFeature = (state: GlobalState) => state.features.wallet; @@ -127,3 +128,10 @@ export const isWalletScreenRefreshingSelector = (state: GlobalState) => isSomeLoadingOrSomeUpdating(paymentsWalletUserMethodsSelector(state)) || isSomeLoadingOrSomeUpdating(idPayWalletInitiativeListSelector(state)) || isSomeLoadingOrSomeUpdating(cgnDetailSelector(state)); + +/** + * Selector that handles the visibility of the category filter tabs shown on the wallet screen. + */ +export const shouldRenderCategoryFiltersSelector = (state: GlobalState) => + selectWalletCategories(state).size > 1 && + !itwIsWalletInstanceStatusFailureSelector(state);