Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(IT Wallet): [SIW-1639] Show error banner when wallet instance status check fails #6568

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<View style={styles.bannerContainer}>
<Icon name="warningFilled" />
<BodySmall style={styles.textCenter}>
{I18n.t("features.itWallet.generic.walletNotAvailable.message")}
</BodySmall>
<BodySmall style={styles.textCenter}>
{I18n.t("features.itWallet.generic.walletNotAvailable.cta")}
</BodySmall>
</View>
);
};

const styles = StyleSheet.create({
bannerContainer: {
padding: IOAlertSpacing[1],
marginVertical: 16,
backgroundColor: IOColors["grey-50"],
borderRadius: 8,
alignItems: "center",
gap: 8
},
textCenter: {
textAlign: "center"
}
});
4 changes: 3 additions & 1 deletion ts/features/itwallet/common/store/selectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,13 +50,14 @@ 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
* @returns true if the banner should be visible, false otherwise
*/
export const itwShouldRenderWalletReadyBannerSelector = (state: GlobalState) =>
itwLifecycleIsValidSelector(state) &&
!itwIsWalletInstanceStatusFailureSelector(state) &&
itwCredentialsEidStatusSelector(state) !== "jwtExpired" &&
itwIsWalletEmptySelector(state);
13 changes: 12 additions & 1 deletion ts/features/itwallet/common/utils/itwTypesUtils.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -43,6 +47,13 @@ export type ParsedStatusAttestation = Awaited<
ReturnType<typeof Credential.Status.verifyAndParseStatusAttestation>
>["parsedStatusAttestation"]["payload"];

/**
* Alias for the WalletInstanceStatus type
*/
export type WalletInstanceStatus = Awaited<
ReturnType<typeof WalletInstance.getWalletInstanceStatus>
>;

export type StoredStatusAttestation =
| {
credentialStatus: "valid";
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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 (
Expand Down
15 changes: 12 additions & 3 deletions ts/features/itwallet/walletInstance/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -7,6 +9,13 @@ export const itwWalletInstanceAttestationStore = createStandardAction(
"ITW_WALLET_INSTANCE_ATTESTATION_STORE"
)<string>();

export type ItwWalletInstanceActions = ActionType<
typeof itwWalletInstanceAttestationStore
>;
/**
* This action update the Wallet Instance Status
*/
export const itwUpdateWalletInstanceStatus = createStandardAction(
"ITW_WALLET_INSTANCE_STATUS_UPDATE"
)<WalletInstanceStatus | FailureStatus>();

export type ItwWalletInstanceActions =
| ActionType<typeof itwWalletInstanceAttestationStore>
| ActionType<typeof itwUpdateWalletInstanceStatus>;
27 changes: 25 additions & 2 deletions ts/features/itwallet/walletInstance/store/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 };

Expand Down Expand Up @@ -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;
17 changes: 15 additions & 2 deletions ts/features/wallet/components/WalletCardsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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";
Expand All @@ -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
Expand All @@ -69,17 +74,25 @@ const WalletCardsContainer = () => {
}
return (
<View testID="walletCardsContainerTestID" style={IOStyles.flex}>
{shouldRenderCategory("itw") && <ItwWalletCardsContainer />}
{!isWalletInstanceStatusFailure && shouldRenderCategory("itw") && (
<ItwWalletCardsContainer />
)}
{shouldRenderCategory("other") && <OtherWalletCardsContainer />}
</View>
);
}, [shouldRenderEmptyState, shouldRenderCategory, shouldRenderLoadingState]);
}, [
shouldRenderEmptyState,
shouldRenderCategory,
shouldRenderLoadingState,
isWalletInstanceStatusFailure
]);

return (
<Animated.View
style={IOStyles.flex}
layout={LinearTransition.duration(200)}
>
<ItwWalletNotAvailableBanner />
<ItwDiscoveryBannerStandalone />
{walletContent}
</Animated.View>
Expand Down
8 changes: 4 additions & 4 deletions ts/features/wallet/components/WalletCategoryFilterTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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(
() =>
Expand All @@ -34,7 +34,7 @@ const WalletCategoryFilterTabs = () => {
[selectedCategory]
);

if (categories.size <= 1) {
if (!shouldRender) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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");
Expand Down
Loading
Loading