Skip to content

Commit

Permalink
feat(IT Wallet): [SIW-1824] Show alert if wallet instance is revoked (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
RiccardoMolinari95 authored Dec 19, 2024
1 parent 323996f commit fbe9e87
Show file tree
Hide file tree
Showing 19 changed files with 322 additions and 38 deletions.
4 changes: 1 addition & 3 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -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'
4 changes: 1 addition & 3 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -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'
14 changes: 14 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 0 additions & 5 deletions ts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ exports[`featuresPersistor should match snapshot 1`] = `
},
"walletInstance": {
"attestation": undefined,
"status": undefined,
},
},
"landingBanners": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ exports[`itWalletReducer should match snapshot [if this test fails, remember to
},
"walletInstance": {
"attestation": undefined,
"status": undefined,
},
}
`;
19 changes: 18 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,19 @@ export type ParsedStatusAttestation = Awaited<
ReturnType<typeof Credential.Status.verifyAndParseStatusAttestation>
>["parsedStatusAttestation"]["payload"];

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

/**
* Alias for the WalletInstanceRevocationReason type
*/
export type WalletInstanceRevocationReason =
WalletInstanceStatus["revocation_reason"];

export type StoredStatusAttestation =
| {
credentialStatus: "valid";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -23,6 +24,9 @@ export function* getStatusOrResetWalletInstance(integrityKeyTag: string) {
if (walletInstanceStatus.is_revoked) {
yield* call(handleWalletInstanceResetSaga);
}

// Update wallet instance status
yield* put(itwUpdateWalletInstanceStatus(walletInstanceStatus));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion ts/features/itwallet/machine/credential/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion ts/features/itwallet/machine/eid/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion ts/features/itwallet/trustmark/machine/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
};
14 changes: 11 additions & 3 deletions ts/features/itwallet/walletInstance/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ActionType, createStandardAction } from "typesafe-actions";
import { WalletInstanceStatus } from "../../../common/utils/itwTypesUtils";

/**
* This action stores the Wallet Instance Attestation
Expand All @@ -7,6 +8,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 | undefined>();

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

Expand All @@ -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;
23 changes: 23 additions & 0 deletions ts/features/itwallet/walletInstance/store/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit fbe9e87

Please sign in to comment.