Skip to content

Commit 26bdee5

Browse files
gispadaLazyAfternoons
authored andcommitted
chore(IT Wallet): [SIW-1639] Show error banner when wallet instance status check fails (#6568)
## Short description This PR handles unexpected failures during the request to fetch the Wallet Instance status. When it is not possible to determine the status, the Wallet will not be available. ## List of changes proposed in this pull request - Created `ItwWalletNotAvailableBanner` component - Turned `itWallet.walletInstance.status` into a pot for better async handling (+ store migration) - Hide `ItwWalletReadyBanner` and `ItwWalletCardsContainer` when the Wallet Instance status call failed ## How to test - Get a wallet instance, then fake an error in `api/v1/wallet/wallet-instances/<wallet_instance_id>/status`: you should see the following screen Regression tests: - Get a wallet instance and restart the app: everything should work as before - Get a wallet instance, then revoke it: everything should work as before (with an alert only shown once) <img src="https://github.com/user-attachments/assets/cf8d36d0-0fad-4e5e-a4a8-80b75ce3c72b" width="240" />
1 parent 0645b8d commit 26bdee5

File tree

14 files changed

+179
-51
lines changed

14 files changed

+179
-51
lines changed

locales/en/index.yml

+3
Original file line numberDiff line numberDiff line change
@@ -3300,6 +3300,9 @@ features:
33003300
claimNotAvailable: "Attributo non riconosciuto"
33013301
claimLabelNotAvailable: "Etichetta attributo non presente"
33023302
organizationName: "Nome ente non disponibile"
3303+
walletNotAvailable:
3304+
message: Abbiamo avuto un problema nel recuperare i tuoi documenti.
3305+
cta: Chiudi e riapri l'app per riprovare.
33033306
verifiableCredentials:
33043307
claims:
33053308
uniqueId: "ID univoco"

locales/it/index.yml

+3
Original file line numberDiff line numberDiff line change
@@ -3300,6 +3300,9 @@ features:
33003300
claimNotAvailable: "Attributo non riconosciuto"
33013301
claimLabelNotAvailable: "Etichetta attributo non presente"
33023302
organizationName: "Nome ente non disponibile"
3303+
walletNotAvailable:
3304+
message: Abbiamo avuto un problema nel recuperare i tuoi documenti.
3305+
cta: Chiudi e riapri l'app per riprovare.
33033306
verifiableCredentials:
33043307
claims:
33053308
uniqueId: "ID univoco"

ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap

+3-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ exports[`featuresPersistor should match snapshot 1`] = `
132132
},
133133
"walletInstance": {
134134
"attestation": undefined,
135-
"status": undefined,
135+
"status": {
136+
"kind": "PotNone",
137+
},
136138
},
137139
},
138140
"landingBanners": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from "react";
2+
import { StyleSheet, View } from "react-native";
3+
import {
4+
BodySmall,
5+
IOAlertSpacing,
6+
IOColors,
7+
Icon
8+
} from "@pagopa/io-app-design-system";
9+
import I18n from "../../../../i18n";
10+
import { useIOSelector } from "../../../../store/hooks";
11+
import { itwIsWalletInstanceStatusFailureSelector } from "../../walletInstance/store/selectors";
12+
import { withWalletCategoryFilter } from "../../../wallet/utils";
13+
14+
/**
15+
* Component shown when it is not possible to retrieve the wallet instance status
16+
* from the backend, because of unexpected errors.
17+
*/
18+
export const ItwWalletNotAvailableBanner = withWalletCategoryFilter(
19+
"itw",
20+
() => {
21+
const isWalletInstanceStatusFailure = useIOSelector(
22+
itwIsWalletInstanceStatusFailureSelector
23+
);
24+
25+
if (!isWalletInstanceStatusFailure) {
26+
return null;
27+
}
28+
29+
return (
30+
<View style={styles.bannerContainer}>
31+
<Icon name="warningFilled" />
32+
<BodySmall style={styles.textCenter}>
33+
{I18n.t("features.itWallet.generic.walletNotAvailable.message")}
34+
</BodySmall>
35+
<BodySmall style={styles.textCenter}>
36+
{I18n.t("features.itWallet.generic.walletNotAvailable.cta")}
37+
</BodySmall>
38+
</View>
39+
);
40+
}
41+
);
42+
43+
const styles = StyleSheet.create({
44+
bannerContainer: {
45+
padding: IOAlertSpacing[1],
46+
marginVertical: 16,
47+
backgroundColor: IOColors["grey-50"],
48+
borderRadius: 8,
49+
alignItems: "center",
50+
gap: 8
51+
},
52+
textCenter: {
53+
textAlign: "center"
54+
}
55+
});

ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ exports[`itWalletReducer should match snapshot [if this test fails, remember to
1919
},
2020
"walletInstance": {
2121
"attestation": undefined,
22-
"status": undefined,
22+
"status": {
23+
"kind": "PotNone",
24+
},
2325
},
2426
}
2527
`;

ts/features/itwallet/common/store/selectors/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
itwIsWalletEmptySelector
99
} from "../../../credentials/store/selectors";
1010
import { itwLifecycleIsValidSelector } from "../../../lifecycle/store/selectors";
11+
import { itwIsWalletInstanceStatusFailureSelector } from "../../../walletInstance/store/selectors";
1112
import {
1213
itwIsFeedbackBannerHiddenSelector,
1314
itwIsDiscoveryBannerHiddenSelector
@@ -49,13 +50,14 @@ export const itwShouldRenderFeedbackBannerSelector = (state: GlobalState) =>
4950

5051
/**
5152
* Returns if the wallet ready banner should be visible. The banner is visible if:
52-
* - The Wallet has valid Wallet Instance and a valid eID
53+
* - The Wallet has valid Wallet Instance with a known status, and a valid eID
5354
* - The eID is not expired
5455
* - The Wallet is empty
5556
* @param state the application global state
5657
* @returns true if the banner should be visible, false otherwise
5758
*/
5859
export const itwShouldRenderWalletReadyBannerSelector = (state: GlobalState) =>
5960
itwLifecycleIsValidSelector(state) &&
61+
!itwIsWalletInstanceStatusFailureSelector(state) &&
6062
itwCredentialsEidStatusSelector(state) !== "jwtExpired" &&
6163
itwIsWalletEmptySelector(state);

ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts

+16-11
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,32 @@ import { assert } from "../../../../utils/assert";
66
import { getWalletInstanceStatus } from "../../common/utils/itwAttestationUtils";
77
import { ensureIntegrityServiceIsReady } from "../../common/utils/itwIntegrityUtils";
88
import { itwIntegrityKeyTagSelector } from "../../issuance/store/selectors";
9+
import { itwUpdateWalletInstanceStatus } from "../../walletInstance/store/actions";
910
import { itwLifecycleIsOperationalOrValid } from "../store/selectors";
1011
import { itwIntegritySetServiceIsReady } from "../../issuance/store/actions";
11-
import { itwUpdateWalletInstanceStatus } from "../../walletInstance/store/actions";
12+
import { getNetworkError } from "../../../../utils/errors";
1213
import { handleWalletInstanceResetSaga } from "./handleWalletInstanceResetSaga";
1314

1415
export function* getStatusOrResetWalletInstance(integrityKeyTag: string) {
1516
const sessionToken = yield* select(sessionTokenSelector);
1617
assert(sessionToken, "Missing session token");
1718

18-
const walletInstanceStatus = yield* call(
19-
getWalletInstanceStatus,
20-
integrityKeyTag,
21-
sessionToken
22-
);
19+
try {
20+
const walletInstanceStatus = yield* call(
21+
getWalletInstanceStatus,
22+
integrityKeyTag,
23+
sessionToken
24+
);
25+
26+
if (walletInstanceStatus.is_revoked) {
27+
yield* call(handleWalletInstanceResetSaga);
28+
}
2329

24-
if (walletInstanceStatus.is_revoked) {
25-
yield* call(handleWalletInstanceResetSaga);
30+
// Update wallet instance status
31+
yield* put(itwUpdateWalletInstanceStatus.success(walletInstanceStatus));
32+
} catch (e) {
33+
yield* put(itwUpdateWalletInstanceStatus.failure(getNetworkError(e)));
2634
}
27-
28-
// Update wallet instance status
29-
yield* put(itwUpdateWalletInstanceStatus(walletInstanceStatus));
3035
}
3136

3237
/**

ts/features/itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const useItwWalletInstanceRevocationAlert = () => {
3030
useCallback(() => {
3131
if (walletInstanceStatus?.is_revoked) {
3232
showWalletRevocationAlert(walletInstanceStatus.revocation_reason);
33-
dispatch(itwUpdateWalletInstanceStatus(undefined));
33+
dispatch(itwUpdateWalletInstanceStatus.cancel());
3434
}
3535
}, [walletInstanceStatus, dispatch])
3636
);

ts/features/itwallet/walletInstance/store/actions/index.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { ActionType, createStandardAction } from "typesafe-actions";
1+
import {
2+
ActionType,
3+
createStandardAction,
4+
createAsyncAction
5+
} from "typesafe-actions";
26
import { WalletInstanceStatus } from "../../../common/utils/itwTypesUtils";
7+
import { NetworkError } from "../../../../../utils/errors";
38

49
/**
510
* This action stores the Wallet Instance Attestation
@@ -9,11 +14,14 @@ export const itwWalletInstanceAttestationStore = createStandardAction(
914
)<string>();
1015

1116
/**
12-
* This action update the Wallet Instance Status
17+
* This action handles the Wallet Instance Status fetch
1318
*/
14-
export const itwUpdateWalletInstanceStatus = createStandardAction(
15-
"ITW_WALLET_INSTANCE_STATUS_UPDATE"
16-
)<WalletInstanceStatus | undefined>();
19+
export const itwUpdateWalletInstanceStatus = createAsyncAction(
20+
"ITW_WALLET_INSTANCE_STATUS_REQUEST",
21+
"ITW_WALLET_INSTANCE_STATUS_SUCCESS",
22+
"ITW_WALLET_INSTANCE_STATUS_FAILURE",
23+
"ITW_WALLET_INSTANCE_STATUS_CANCEL"
24+
)<void, WalletInstanceStatus, NetworkError, undefined>();
1725

1826
export type ItwWalletInstanceActions =
1927
| ActionType<typeof itwWalletInstanceAttestationStore>

ts/features/itwallet/walletInstance/store/reducers/index.ts

+46-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { PersistConfig, persistReducer } from "redux-persist";
1+
import {
2+
MigrationManifest,
3+
PersistConfig,
4+
PersistedState,
5+
createMigrate,
6+
persistReducer
7+
} from "redux-persist";
28
import { getType } from "typesafe-actions";
9+
import * as pot from "@pagopa/ts-commons/lib/pot";
310
import { Action } from "../../../../../store/actions/types";
411
import itwCreateSecureStorage from "../../../common/store/storages/itwSecureStorage";
512
import { itwLifecycleStoresReset } from "../../../lifecycle/store/actions";
@@ -8,18 +15,34 @@ import {
815
itwUpdateWalletInstanceStatus
916
} from "../actions";
1017
import { WalletInstanceStatus } from "../../../common/utils/itwTypesUtils";
18+
import { NetworkError } from "../../../../../utils/errors";
19+
import { isDevEnv } from "../../../../../utils/environment";
1120

1221
export type ItwWalletInstanceState = {
1322
attestation: string | undefined;
14-
status: WalletInstanceStatus | undefined;
23+
status: pot.Pot<WalletInstanceStatus, NetworkError>;
1524
};
1625

1726
export const itwWalletInstanceInitialState: ItwWalletInstanceState = {
1827
attestation: undefined,
19-
status: undefined
28+
status: pot.none
2029
};
2130

22-
const CURRENT_REDUX_ITW_WALLET_INSTANCE_STORE_VERSION = -1;
31+
const CURRENT_REDUX_ITW_WALLET_INSTANCE_STORE_VERSION = 0;
32+
33+
const migrations: MigrationManifest = {
34+
// Convert status into a pot for better async handling
35+
"0": (state): ItwWalletInstanceState & PersistedState => {
36+
const prevState = state as PersistedState & {
37+
attestation: string | undefined;
38+
status: WalletInstanceStatus | undefined;
39+
};
40+
return {
41+
...prevState,
42+
status: prevState.status ? pot.some(prevState.status) : pot.none
43+
};
44+
}
45+
};
2346

2447
const reducer = (
2548
state: ItwWalletInstanceState = itwWalletInstanceInitialState,
@@ -28,15 +51,29 @@ const reducer = (
2851
switch (action.type) {
2952
case getType(itwWalletInstanceAttestationStore): {
3053
return {
31-
status: undefined,
54+
status: pot.none,
3255
attestation: action.payload
3356
};
3457
}
3558

36-
case getType(itwUpdateWalletInstanceStatus): {
59+
case getType(itwUpdateWalletInstanceStatus.success): {
60+
return {
61+
...state,
62+
status: pot.some(action.payload)
63+
};
64+
}
65+
66+
case getType(itwUpdateWalletInstanceStatus.failure): {
67+
return {
68+
...state,
69+
status: pot.toError(state.status, action.payload)
70+
};
71+
}
72+
73+
case getType(itwUpdateWalletInstanceStatus.cancel): {
3774
return {
3875
...state,
39-
status: action.payload
76+
status: pot.none
4077
};
4178
}
4279

@@ -51,7 +88,8 @@ const reducer = (
5188
const itwWalletInstancePersistConfig: PersistConfig = {
5289
key: "itwWalletInstance",
5390
storage: itwCreateSecureStorage(),
54-
version: CURRENT_REDUX_ITW_WALLET_INSTANCE_STORE_VERSION
91+
version: CURRENT_REDUX_ITW_WALLET_INSTANCE_STORE_VERSION,
92+
migrate: createMigrate(migrations, { debug: isDevEnv })
5593
};
5694

5795
const persistedReducer = persistReducer(

ts/features/itwallet/walletInstance/store/selectors/index.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as O from "fp-ts/lib/Option";
22
import { flow } from "fp-ts/lib/function";
3+
import * as pot from "@pagopa/ts-commons/lib/pot";
34
import { createSelector } from "reselect";
45
import { GlobalState } from "../../../../../store/reducers/types";
56
import { isWalletInstanceAttestationValid } from "../../../common/utils/itwAttestationUtils";
@@ -20,4 +21,11 @@ export const itwIsWalletInstanceAttestationValidSelector = createSelector(
2021

2122
/* Selector to get the wallet instance status */
2223
export const itwWalletInstanceStatusSelector = (state: GlobalState) =>
23-
state.features.itWallet.walletInstance.status;
24+
pot.toUndefined(state.features.itWallet.walletInstance.status);
25+
26+
/**
27+
* Returns true when it was not possible to retrieve the wallet instance status because of unexpected errors,
28+
* hence we cannot know whether the wallet instance is valid or has been revoked.
29+
*/
30+
export const itwIsWalletInstanceStatusFailureSelector = (state: GlobalState) =>
31+
pot.isError(state.features.itWallet.walletInstance.status);

0 commit comments

Comments
 (0)