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);