(
}
if (typeof value === "object" && value !== null) {
+ if (value instanceof Set) {
+ // Set could not be serialized to JSON because values are not stored as properties
+ // For display purposes, we convert it to an array
+ return Array.from(value).map(item =>
+ truncateObjectStrings(item, maxLength)
+ ) as T;
+ }
+
return Object.entries(value).reduce(
(acc, [key, val]) => ({
...acc,
diff --git a/ts/components/debug/withDebugEnabled.tsx b/ts/components/debug/withDebugEnabled.tsx
index 72eb5eccea5..303a6aa45dd 100644
--- a/ts/components/debug/withDebugEnabled.tsx
+++ b/ts/components/debug/withDebugEnabled.tsx
@@ -6,11 +6,13 @@ import { isDebugModeEnabledSelector } from "../../store/reducers/debug";
* This HOC allows to render the wrapped component only if the debug mode is enabled, otherwise returns null (nothing)
*/
export const withDebugEnabled =
- (WrappedComponent: React.ComponentType
) =>
+
>(
+ WrappedComponent: React.ComponentType
+ ) =>
(props: P) => {
const isDebug = useIOSelector(isDebugModeEnabledSelector);
if (!isDebug) {
return null;
}
- return ;
+ return ;
};
diff --git a/ts/config.ts b/ts/config.ts
index fa847bbb3ad..505771f19eb 100644
--- a/ts/config.ts
+++ b/ts/config.ts
@@ -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")
-);
diff --git a/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap b/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap
index 527cdcd64ce..81dd7b4bcb0 100644
--- a/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap
+++ b/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap
@@ -132,6 +132,7 @@ exports[`featuresPersistor should match snapshot 1`] = `
},
"walletInstance": {
"attestation": undefined,
+ "status": undefined,
},
},
"landingBanners": {
diff --git a/ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap b/ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap
index 7fb4ec75a32..e6f11fb8d17 100644
--- a/ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap
+++ b/ts/features/itwallet/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap
@@ -19,6 +19,7 @@ exports[`itWalletReducer should match snapshot [if this test fails, remember to
},
"walletInstance": {
"attestation": undefined,
+ "status": undefined,
},
}
`;
diff --git a/ts/features/itwallet/common/utils/itwAttestationUtils.ts b/ts/features/itwallet/common/utils/itwAttestationUtils.ts
index a28f06739b5..985163f6dfb 100644
--- a/ts/features/itwallet/common/utils/itwAttestationUtils.ts
+++ b/ts/features/itwallet/common/utils/itwAttestationUtils.ts
@@ -23,6 +23,7 @@ export const getIntegrityHardwareKeyTag = async (): Promise =>
/**
* Register a new wallet instance with hardwareKeyTag.
* @param hardwareKeyTag - the hardware key tag of the integrity Context
+ * @param sessionToken - the session token to use for the API calls
*/
export const registerWalletInstance = async (
hardwareKeyTag: string,
@@ -42,6 +43,7 @@ export const registerWalletInstance = async (
/**
* Getter for the wallet attestation binded to the wallet instance created with the given hardwareKeyTag.
* @param hardwareKeyTag - the hardware key tag of the wallet instance
+ * @param sessionToken - the session token to use for the API calls
* @return the wallet attestation and the related key tag
*/
export const getAttestation = async (
@@ -81,6 +83,7 @@ export const isWalletInstanceAttestationValid = (
* Get the wallet instance status from the Wallet Provider.
* This operation is more lightweight than getting a new attestation to check the status.
* @param hardwareKeyTag The hardware key tag used to create the wallet instance
+ * @param sessionToken The session token to use for the API calls
*/
export const getWalletInstanceStatus = (
hardwareKeyTag: string,
diff --git a/ts/features/itwallet/common/utils/itwTypesUtils.ts b/ts/features/itwallet/common/utils/itwTypesUtils.ts
index fe04304f663..3fab058e345 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,19 @@ export type ParsedStatusAttestation = Awaited<
ReturnType
>["parsedStatusAttestation"]["payload"];
+/**
+ * Alias for the WalletInstanceStatus type
+ */
+export type WalletInstanceStatus = Awaited<
+ ReturnType
+>;
+
+/**
+ * Alias for the WalletInstanceRevocationReason type
+ */
+export type WalletInstanceRevocationReason =
+ WalletInstanceStatus["revocation_reason"];
+
export type StoredStatusAttestation =
| {
credentialStatus: "valid";
diff --git a/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts b/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts
index fdf253d364c..574090dea19 100644
--- a/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts
+++ b/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts
@@ -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
diff --git a/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts b/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts
index 779a11a929e..07d6dc12719 100644
--- a/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts
+++ b/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts
@@ -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) {
@@ -23,6 +24,9 @@ export function* getStatusOrResetWalletInstance(integrityKeyTag: string) {
if (walletInstanceStatus.is_revoked) {
yield* call(handleWalletInstanceResetSaga);
}
+
+ // Update wallet instance status
+ yield* put(itwUpdateWalletInstanceStatus(walletInstanceStatus));
}
/**
diff --git a/ts/features/itwallet/machine/credential/actions.ts b/ts/features/itwallet/machine/credential/actions.ts
index f5f1eaf9990..ee43befc3b9 100644
--- a/ts/features/itwallet/machine/credential/actions.ts
+++ b/ts/features/itwallet/machine/credential/actions.ts
@@ -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";
diff --git a/ts/features/itwallet/machine/eid/__tests__/machine.test.ts b/ts/features/itwallet/machine/eid/__tests__/machine.test.ts
index c7d51a9c7ab..751df57822b 100644
--- a/ts/features/itwallet/machine/eid/__tests__/machine.test.ts
+++ b/ts/features/itwallet/machine/eid/__tests__/machine.test.ts
@@ -40,6 +40,7 @@ describe("itwEidIssuanceMachine", () => {
const navigateToNfcInstructionsScreen = jest.fn();
const navigateToCieIdLoginScreen = jest.fn();
const storeIntegrityKeyTag = jest.fn();
+ const cleanupIntegrityKeyTag = jest.fn();
const storeWalletInstanceAttestation = jest.fn();
const storeEidCredential = jest.fn();
const closeIssuance = jest.fn();
@@ -82,6 +83,7 @@ describe("itwEidIssuanceMachine", () => {
navigateToNfcInstructionsScreen,
navigateToCieIdLoginScreen,
storeIntegrityKeyTag,
+ cleanupIntegrityKeyTag,
storeWalletInstanceAttestation,
storeEidCredential,
closeIssuance,
@@ -982,4 +984,49 @@ describe("itwEidIssuanceMachine", () => {
expect(actor.getSnapshot().value).toStrictEqual("Idle");
});
+
+ it("should cleanup integrity key tag and fail when obtaining Wallet Instance Attestation fails", async () => {
+ const actor = createActor(mockedMachine);
+ actor.start();
+
+ await waitFor(() => expect(onInit).toHaveBeenCalledTimes(1));
+
+ expect(actor.getSnapshot().value).toStrictEqual("Idle");
+ expect(actor.getSnapshot().context).toStrictEqual(InitialContext);
+ expect(actor.getSnapshot().tags).toStrictEqual(new Set());
+
+ /**
+ * Start eID issuance
+ */
+ actor.send({ type: "start" });
+
+ expect(actor.getSnapshot().value).toStrictEqual("TosAcceptance");
+ expect(actor.getSnapshot().tags).toStrictEqual(new Set());
+ expect(navigateToTosScreen).toHaveBeenCalledTimes(1);
+
+ /**
+ * Accept TOS and request WIA
+ */
+
+ createWalletInstance.mockImplementation(() =>
+ Promise.resolve(T_INTEGRITY_KEY)
+ );
+ getWalletAttestation.mockImplementation(() => Promise.reject({})); // Simulate failure
+ isSessionExpired.mockImplementation(() => false); // Session not expired
+
+ actor.send({ type: "accept-tos" });
+
+ expect(actor.getSnapshot().value).toStrictEqual("WalletInstanceCreation");
+ expect(actor.getSnapshot().tags).toStrictEqual(new Set([ItwTags.Loading]));
+
+ await waitFor(() => expect(createWalletInstance).toHaveBeenCalledTimes(1));
+ await waitFor(() => expect(getWalletAttestation).toHaveBeenCalledTimes(1));
+
+ // Wallet Instance Attestation failure triggers cleanupIntegrityKeyTag
+ expect(cleanupIntegrityKeyTag).toHaveBeenCalledTimes(1);
+
+ // Check that the machine transitions to Failure state
+ expect(actor.getSnapshot().value).toStrictEqual("Failure");
+ expect(actor.getSnapshot().tags).toStrictEqual(new Set());
+ });
});
diff --git a/ts/features/itwallet/machine/eid/actions.ts b/ts/features/itwallet/machine/eid/actions.ts
index d7581cc2b7a..2572a537d9f 100644
--- a/ts/features/itwallet/machine/eid/actions.ts
+++ b/ts/features/itwallet/machine/eid/actions.ts
@@ -9,7 +9,10 @@ import { checkCurrentSession } from "../../../../store/actions/authentication";
import { useIOStore } from "../../../../store/hooks";
import { assert } from "../../../../utils/assert";
import { itwCredentialsStore } from "../../credentials/store/actions";
-import { itwStoreIntegrityKeyTag } from "../../issuance/store/actions";
+import {
+ itwRemoveIntegrityKeyTag,
+ itwStoreIntegrityKeyTag
+} from "../../issuance/store/actions";
import {
itwLifecycleStateUpdated,
itwLifecycleWalletReset
@@ -22,8 +25,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";
@@ -168,6 +171,11 @@ export const createEidIssuanceActionsImplementation = (
store.dispatch(itwStoreIntegrityKeyTag(context.integrityKeyTag));
},
+ cleanupIntegrityKeyTag: () => {
+ // Remove the integrity key tag from the store
+ store.dispatch(itwRemoveIntegrityKeyTag());
+ },
+
storeWalletInstanceAttestation: ({
context
}: ActionArgs) => {
diff --git a/ts/features/itwallet/machine/eid/machine.ts b/ts/features/itwallet/machine/eid/machine.ts
index 3da95b4605c..1d381db258f 100644
--- a/ts/features/itwallet/machine/eid/machine.ts
+++ b/ts/features/itwallet/machine/eid/machine.ts
@@ -43,6 +43,7 @@ export const itwEidIssuanceMachine = setup({
navigateToNfcInstructionsScreen: notImplemented,
navigateToWalletRevocationScreen: notImplemented,
storeIntegrityKeyTag: notImplemented,
+ cleanupIntegrityKeyTag: notImplemented,
storeWalletInstanceAttestation: notImplemented,
storeEidCredential: notImplemented,
closeIssuance: notImplemented,
@@ -225,7 +226,7 @@ export const itwEidIssuanceMachine = setup({
target: "#itwEidIssuanceMachine.TosAcceptance"
},
{
- actions: "setFailure",
+ actions: ["setFailure", "cleanupIntegrityKeyTag"],
target: "#itwEidIssuanceMachine.Failure"
}
]
diff --git a/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx b/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx
index 0efb8a6768f..cdaf466893f 100644
--- a/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx
+++ b/ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx
@@ -112,7 +112,7 @@ const ItwCredentialOnboardingSection = () => {
);
return (
- <>
+
@@ -134,7 +134,7 @@ const ItwCredentialOnboardingSection = () => {
/>
))}
- >
+
);
};
@@ -175,7 +175,7 @@ const OtherCardsOnboardingSection = (props: { showTitle?: boolean }) => {
);
return (
- <>
+
{props.showTitle && (
{
onPress={navigateToPaymentMethodOnboarding}
/>
- >
+
);
};
diff --git a/ts/features/itwallet/trustmark/machine/actions.ts b/ts/features/itwallet/trustmark/machine/actions.ts
index 929d2d32104..145a4ba00fd 100644
--- a/ts/features/itwallet/trustmark/machine/actions.ts
+++ b/ts/features/itwallet/trustmark/machine/actions.ts
@@ -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 = (
diff --git a/ts/features/itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert.ts b/ts/features/itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert.ts
new file mode 100644
index 00000000000..15698a1baf2
--- /dev/null
+++ b/ts/features/itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert.ts
@@ -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;
+ }
+};
diff --git a/ts/features/itwallet/walletInstance/store/actions/index.ts b/ts/features/itwallet/walletInstance/store/actions/index.ts
index 3d0c05b05c2..cbf6db80535 100644
--- a/ts/features/itwallet/walletInstance/store/actions/index.ts
+++ b/ts/features/itwallet/walletInstance/store/actions/index.ts
@@ -1,4 +1,5 @@
import { ActionType, createStandardAction } from "typesafe-actions";
+import { WalletInstanceStatus } from "../../../common/utils/itwTypesUtils";
/**
* This action stores the Wallet Instance Attestation
@@ -7,6 +8,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..778d134d4fb 100644
--- a/ts/features/itwallet/walletInstance/store/reducers/index.ts
+++ b/ts/features/itwallet/walletInstance/store/reducers/index.ts
@@ -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;
@@ -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 };
@@ -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;
diff --git a/ts/features/itwallet/walletInstance/store/selectors/index.ts b/ts/features/itwallet/walletInstance/store/selectors/index.ts
new file mode 100644
index 00000000000..85a98084d61
--- /dev/null
+++ b/ts/features/itwallet/walletInstance/store/selectors/index.ts
@@ -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;
diff --git a/ts/features/payments/receipts/screens/ReceiptListScreen.tsx b/ts/features/payments/receipts/screens/ReceiptListScreen.tsx
index 95d1f2d51f8..789b1386e7c 100644
--- a/ts/features/payments/receipts/screens/ReceiptListScreen.tsx
+++ b/ts/features/payments/receipts/screens/ReceiptListScreen.tsx
@@ -12,31 +12,39 @@ import Animated, {
useSharedValue
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { useIODispatch, useIOSelector } from "../../../../store/hooks";
-import { PaymentsReceiptParamsList } from "../navigation/params";
-import { getPaymentsReceiptAction } from "../store/actions";
-import { walletReceiptListPotSelector } from "../store/selectors";
-import { useIONavigation } from "../../../../navigation/params/AppParamsList";
-import { isPaymentsTransactionsEmptySelector } from "../../home/store/selectors";
-import { ReceiptListItemTransaction } from "../components/ReceiptListItemTransaction";
+import { NoticeListItem } from "../../../../../definitions/pagopa/biz-events/NoticeListItem";
+import {
+ OperationResultScreenContent,
+ OperationResultScreenContentProps
+} from "../../../../components/screens/OperationResultScreenContent";
import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel";
-import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender";
-import { groupTransactionsByMonth } from "../utils";
import I18n from "../../../../i18n";
-import { PaymentsReceiptRoutes } from "../navigation/routes";
-import { NoticeListItem } from "../../../../../definitions/pagopa/biz-events/NoticeListItem";
+import { useIONavigation } from "../../../../navigation/params/AppParamsList";
+import { useIODispatch, useIOSelector } from "../../../../store/hooks";
+import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender";
+import { isPaymentsTransactionsEmptySelector } from "../../home/store/selectors";
import * as analytics from "../analytics";
-import { ReceiptsCategoryFilter } from "../types";
-import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent";
import { ReceiptFadeInOutAnimationView } from "../components/ReceiptFadeInOutAnimationView";
+import { ReceiptListItemTransaction } from "../components/ReceiptListItemTransaction";
import { ReceiptLoadingList } from "../components/ReceiptLoadingList";
import { ReceiptSectionListHeader } from "../components/ReceiptSectionListHeader";
+import { PaymentsReceiptParamsList } from "../navigation/params";
+import { PaymentsReceiptRoutes } from "../navigation/routes";
+import { getPaymentsReceiptAction } from "../store/actions";
+import { walletReceiptListPotSelector } from "../store/selectors";
+import { ReceiptsCategoryFilter } from "../types";
+import { groupTransactionsByMonth } from "../utils";
export type ReceiptListScreenProps = RouteProp<
PaymentsReceiptParamsList,
"PAYMENT_RECEIPT_DETAILS"
>;
+type OperationResultEmptyProps = Pick<
+ OperationResultScreenContentProps,
+ "title" | "subtitle" | "pictogram"
+>;
+
const AnimatedSectionList = Animated.createAnimatedComponent(
SectionList as new () => SectionList
);
@@ -59,7 +67,6 @@ const ReceiptListScreen = () => {
const transactionsPot = useIOSelector(walletReceiptListPotSelector);
const isEmpty = useIOSelector(isPaymentsTransactionsEmptySelector);
-
const isLoading = pot.isLoading(transactionsPot);
const handleNavigateToTransactionDetails = (transaction: NoticeListItem) => {
@@ -163,14 +170,23 @@ const ReceiptListScreen = () => {
>
);
+ const emptyProps: OperationResultEmptyProps =
+ noticeCategory === "payer"
+ ? {
+ title: I18n.t("features.payments.transactions.list.emptyPayer.title"),
+ pictogram: "empty"
+ }
+ : {
+ title: I18n.t("features.payments.transactions.list.empty.title"),
+ subtitle: I18n.t(
+ "features.payments.transactions.list.empty.subtitle"
+ ),
+ pictogram: "emptyArchive"
+ };
+
const EmptyStateList = isEmpty ? (
-
+
) : undefined;
diff --git a/ts/features/wallet/components/WalletCardsContainer.tsx b/ts/features/wallet/components/WalletCardsContainer.tsx
index 394dc54ee19..562c74fec75 100644
--- a/ts/features/wallet/components/WalletCardsContainer.tsx
+++ b/ts/features/wallet/components/WalletCardsContainer.tsx
@@ -3,12 +3,12 @@ import { useFocusEffect } from "@react-navigation/native";
import * as React from "react";
import { View } from "react-native";
import Animated, { LinearTransition } from "react-native-reanimated";
+import { useDebugInfo } from "../../../hooks/useDebugInfo";
import I18n from "../../../i18n";
import { useIONavigation } from "../../../navigation/params/AppParamsList";
import { useIOSelector } from "../../../store/hooks";
import { isItwEnabledSelector } from "../../../store/reducers/backendStatus/remoteConfig";
import { useIOBottomSheetAutoresizableModal } from "../../../utils/hooks/bottomSheet";
-import { ItwDiscoveryBannerStandalone } from "../../itwallet/common/components/discoveryBanner/ItwDiscoveryBannerStandalone";
import {
ItwEidInfoBottomSheetContent,
ItwEidInfoBottomSheetTitle
@@ -16,21 +16,22 @@ import {
import { ItwEidLifecycleAlert } from "../../itwallet/common/components/ItwEidLifecycleAlert";
import { ItwFeedbackBanner } from "../../itwallet/common/components/ItwFeedbackBanner";
import { ItwWalletReadyBanner } from "../../itwallet/common/components/ItwWalletReadyBanner";
+import { ItwDiscoveryBannerStandalone } from "../../itwallet/common/components/discoveryBanner/ItwDiscoveryBannerStandalone";
import { itwCredentialsEidStatusSelector } from "../../itwallet/credentials/store/selectors";
import { itwLifecycleIsValidSelector } from "../../itwallet/lifecycle/store/selectors";
+import { useItwWalletInstanceRevocationAlert } from "../../itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert";
import {
isWalletEmptySelector,
- selectIsWalletCardsLoading,
+ selectIsWalletLoading,
+ selectWalletCardsByCategory,
selectWalletCategories,
- selectWalletCategoryFilter,
- selectWalletItwCards,
selectWalletOtherCards,
shouldRenderWalletEmptyStateSelector
} from "../store/selectors";
-import { WalletCardCategoryFilter } from "../types";
+import { withWalletCategoryFilter } from "../utils";
+import { WalletCardSkeleton } from "./WalletCardSkeleton";
import { WalletCardsCategoryContainer } from "./WalletCardsCategoryContainer";
import { WalletCardsCategoryRetryErrorBanner } from "./WalletCardsCategoryRetryErrorBanner";
-import { WalletCardSkeleton } from "./WalletCardSkeleton";
import { WalletEmptyScreenContent } from "./WalletEmptyScreenContent";
const EID_INFO_BOTTOM_PADDING = 128;
@@ -41,24 +42,18 @@ const EID_INFO_BOTTOM_PADDING = 128;
* and the empty state
*/
const WalletCardsContainer = () => {
- const isLoading = useIOSelector(selectIsWalletCardsLoading);
+ const isLoading = useIOSelector(selectIsWalletLoading);
const isWalletEmpty = useIOSelector(isWalletEmptySelector);
- const selectedCategory = useIOSelector(selectWalletCategoryFilter);
const shouldRenderEmptyState = useIOSelector(
shouldRenderWalletEmptyStateSelector
);
+ useItwWalletInstanceRevocationAlert();
+
// Loading state is only displayed if there is the initial loading and there are no cards or
// placeholders in the wallet
const shouldRenderLoadingState = isLoading && isWalletEmpty;
- // Returns true if no category filter is selected or if the filter matches the given category
- const shouldRenderCategory = React.useCallback(
- (filter: WalletCardCategoryFilter): boolean =>
- selectedCategory === undefined || selectedCategory === filter,
- [selectedCategory]
- );
-
// Content to render in the wallet screen, based on the current state
const walletContent = React.useMemo(() => {
if (shouldRenderLoadingState) {
@@ -69,11 +64,11 @@ const WalletCardsContainer = () => {
}
return (
- {shouldRenderCategory("itw") && }
- {shouldRenderCategory("other") && }
+
+
);
- }, [shouldRenderEmptyState, shouldRenderCategory, shouldRenderLoadingState]);
+ }, [shouldRenderEmptyState, shouldRenderLoadingState]);
return (
{
);
};
+/**
+ * Skeleton for the wallet cards container
+ */
const WalletCardsContainerSkeleton = () => (
<>
@@ -94,15 +92,29 @@ const WalletCardsContainerSkeleton = () => (
>
);
-const ItwWalletCardsContainer = () => {
+/**
+ * Card container for the ITW credentials
+ */
+const ItwWalletCardsContainer = withWalletCategoryFilter("itw", () => {
const navigation = useIONavigation();
- const cards = useIOSelector(selectWalletItwCards);
+ const cards = useIOSelector(state =>
+ selectWalletCardsByCategory(state, "itw")
+ );
const isItwValid = useIOSelector(itwLifecycleIsValidSelector);
const isItwEnabled = useIOSelector(isItwEnabledSelector);
const eidStatus = useIOSelector(itwCredentialsEidStatusSelector);
const isEidExpired = eidStatus === "jwtExpired";
+ useDebugInfo({
+ itw: {
+ isItwValid,
+ isItwEnabled,
+ eidStatus,
+ cards
+ }
+ });
+
const eidInfoBottomSheet = useIOBottomSheetAutoresizableModal(
{
title: ,
@@ -169,12 +181,21 @@ const ItwWalletCardsContainer = () => {
{isItwValid && eidInfoBottomSheet.bottomSheet}
>
);
-};
+});
-const OtherWalletCardsContainer = () => {
+/**
+ * Card container for the other cards (payments, bonus, etc.)
+ */
+const OtherWalletCardsContainer = withWalletCategoryFilter("other", () => {
const cards = useIOSelector(selectWalletOtherCards);
const categories = useIOSelector(selectWalletCategories);
+ useDebugInfo({
+ other: {
+ cards
+ }
+ });
+
const sectionHeader = React.useMemo((): ListItemHeader | undefined => {
// The section header must be displayed only if there are more categories
if (categories.size <= 1) {
@@ -200,7 +221,7 @@ const OtherWalletCardsContainer = () => {
bottomElement={}
/>
);
-};
+});
export {
ItwWalletCardsContainer,
diff --git a/ts/features/wallet/components/WalletCategoryFilterTabs.tsx b/ts/features/wallet/components/WalletCategoryFilterTabs.tsx
index 9cec1e0534e..10b0921f266 100644
--- a/ts/features/wallet/components/WalletCategoryFilterTabs.tsx
+++ b/ts/features/wallet/components/WalletCategoryFilterTabs.tsx
@@ -10,10 +10,11 @@ import { useIODispatch, useIOSelector } from "../../../store/hooks";
import { trackWalletCategoryFilter } from "../../itwallet/analytics";
import { walletSetCategoryFilter } from "../store/actions/preferences";
import {
- selectWalletCategories,
+ isWalletCategoryFilteringEnabledSelector,
selectWalletCategoryFilter
} from "../store/selectors";
import { walletCardCategoryFilters } from "../types";
+import { useDebugInfo } from "../../../hooks/useDebugInfo";
/**
* Renders filter tabs to categorize cards on the wallet home screen.
@@ -23,18 +24,27 @@ import { walletCardCategoryFilters } from "../types";
const WalletCategoryFilterTabs = () => {
const dispatch = useIODispatch();
- const selectedCategory = useIOSelector(selectWalletCategoryFilter);
- const categories = useIOSelector(selectWalletCategories);
+ const categoryFilter = useIOSelector(selectWalletCategoryFilter);
+ const isFilteringEnabled = useIOSelector(
+ isWalletCategoryFilteringEnabledSelector
+ );
+
+ useDebugInfo({
+ wallet: {
+ isFilteringEnabled,
+ categoryFilter
+ }
+ });
const selectedIndex = React.useMemo(
() =>
- selectedCategory
- ? walletCardCategoryFilters.indexOf(selectedCategory) + 1
+ categoryFilter
+ ? walletCardCategoryFilters.indexOf(categoryFilter) + 1
: 0,
- [selectedCategory]
+ [categoryFilter]
);
- if (categories.size <= 1) {
+ if (!isFilteringEnabled) {
return null;
}
diff --git a/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx b/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx
index 45aabf4fb3b..13b5dec74bd 100644
--- a/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx
+++ b/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx
@@ -2,6 +2,7 @@ import * as O from "fp-ts/lib/Option";
import _ from "lodash";
import * as React from "react";
import configureMockStore from "redux-mock-store";
+import { Alert } from "react-native";
import ROUTES from "../../../../navigation/routes";
import { applicationChangeState } from "../../../../store/actions/application";
import { appReducer } from "../../../../store/reducers";
@@ -16,6 +17,7 @@ import {
import { ItwJwtCredentialStatus } from "../../../itwallet/common/utils/itwTypesUtils";
import * as itwCredentialsSelectors from "../../../itwallet/credentials/store/selectors";
import * as itwLifecycleSelectors from "../../../itwallet/lifecycle/store/selectors";
+import * as itwWalletInstanceSelectors from "../../../itwallet/walletInstance/store/selectors";
import { WalletCardsState } from "../../store/reducers/cards";
import * as walletSelectors from "../../store/selectors";
import { WalletCard } from "../../types";
@@ -24,7 +26,9 @@ import {
OtherWalletCardsContainer,
WalletCardsContainer
} from "../WalletCardsContainer";
+import I18n from "../../../../i18n";
+jest.spyOn(Alert, "alert");
jest.mock("react-native-reanimated", () => ({
...require("react-native-reanimated/mock"),
useReducedMotion: jest.fn,
@@ -91,7 +95,7 @@ describe("WalletCardsContainer", () => {
it("should render the loading screen", () => {
jest
- .spyOn(walletSelectors, "selectIsWalletCardsLoading")
+ .spyOn(walletSelectors, "selectIsWalletLoading")
.mockImplementation(() => true);
jest
.spyOn(walletSelectors, "selectWalletCategoryFilter")
@@ -113,7 +117,7 @@ describe("WalletCardsContainer", () => {
it("should render the empty screen", () => {
jest
- .spyOn(walletSelectors, "selectIsWalletCardsLoading")
+ .spyOn(walletSelectors, "selectIsWalletLoading")
.mockImplementation(() => false);
jest
.spyOn(walletSelectors, "selectWalletCategoryFilter")
@@ -146,14 +150,14 @@ describe("WalletCardsContainer", () => {
.mockImplementation(() => [T_CARDS["1"], T_CARDS["2"], T_CARDS["3"]]);
jest
- .spyOn(walletSelectors, "selectWalletItwCards")
+ .spyOn(walletSelectors, "selectWalletCardsByCategory")
.mockImplementation(() => [T_CARDS["4"], T_CARDS["5"]]);
jest
.spyOn(configSelectors, "isItwEnabledSelector")
.mockImplementation(() => true);
jest
- .spyOn(walletSelectors, "selectIsWalletCardsLoading")
+ .spyOn(walletSelectors, "selectIsWalletLoading")
.mockImplementation(() => false);
jest
.spyOn(walletSelectors, "selectWalletCategoryFilter")
@@ -189,7 +193,7 @@ describe("WalletCardsContainer", () => {
.mockImplementation(() => true);
jest
- .spyOn(walletSelectors, "selectIsWalletCardsLoading")
+ .spyOn(walletSelectors, "selectIsWalletLoading")
.mockImplementation(() => isLoading);
jest
.spyOn(walletSelectors, "shouldRenderWalletEmptyStateSelector")
@@ -240,7 +244,7 @@ describe("ItwWalletCardsContainer", () => {
.spyOn(configSelectors, "isItwEnabledSelector")
.mockImplementation(() => true);
jest
- .spyOn(walletSelectors, "selectWalletItwCards")
+ .spyOn(walletSelectors, "selectWalletCardsByCategory")
.mockImplementation(() => [T_CARDS["4"], T_CARDS["5"]]);
const { queryByTestId } = renderComponent(ItwWalletCardsContainer);
@@ -357,6 +361,112 @@ describe("OtherWalletCardsContainer", () => {
expect(queryByTestId(`walletCardTestID_cgn_cgn_3`)).not.toBeNull();
expect(queryByTestId(`walletCardTestID_itw_placeholder_4`)).not.toBeNull();
});
+
+ it("should not show alert if not revoked", () => {
+ jest
+ .spyOn(itwWalletInstanceSelectors, "itwWalletInstanceStatusSelector")
+ .mockImplementation(() => ({
+ id: "39cc62ab-1df0-4a9d-974d-4c58173a1750",
+ is_revoked: false,
+ revocation_reason: undefined
+ }));
+
+ renderComponent(WalletCardsContainer);
+
+ expect(Alert.alert).not.toHaveBeenCalled();
+ });
+
+ it("should show alert for NEW_WALLET_INSTANCE_CREATED", () => {
+ jest
+ .spyOn(itwWalletInstanceSelectors, "itwWalletInstanceStatusSelector")
+ .mockImplementation(() => ({
+ id: "39cc62ab-1df0-4a9d-974d-4c58173a1750",
+ is_revoked: true,
+ revocation_reason: "NEW_WALLET_INSTANCE_CREATED"
+ }));
+
+ renderComponent(WalletCardsContainer);
+
+ expect(Alert.alert).toHaveBeenCalledWith(
+ I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.newWalletInstanceCreated.title"
+ ),
+ I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.newWalletInstanceCreated.content"
+ ),
+ [
+ {
+ text: I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.closeButton"
+ )
+ },
+ {
+ text: I18n.t("features.itWallet.walletInstanceRevoked.alert.cta"),
+ onPress: expect.any(Function)
+ }
+ ]
+ );
+ });
+
+ it("should show alert for CERTIFICATE_REVOKED_BY_ISSUER", () => {
+ jest
+ .spyOn(itwWalletInstanceSelectors, "itwWalletInstanceStatusSelector")
+ .mockImplementation(() => ({
+ id: "39cc62ab-1df0-4a9d-974d-4c58173a1750",
+ is_revoked: true,
+ revocation_reason: "CERTIFICATE_REVOKED_BY_ISSUER"
+ }));
+
+ renderComponent(WalletCardsContainer);
+
+ expect(Alert.alert).toHaveBeenCalledWith(
+ I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.revokedByWalletProvider.title"
+ ),
+ I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.revokedByWalletProvider.content"
+ ),
+ [
+ {
+ text: I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.closeButton"
+ )
+ },
+ {
+ text: I18n.t("features.itWallet.walletInstanceRevoked.alert.cta"),
+ onPress: expect.any(Function)
+ }
+ ]
+ );
+ });
+
+ it("should show alert for REVOKED_BY_USER", () => {
+ jest
+ .spyOn(itwWalletInstanceSelectors, "itwWalletInstanceStatusSelector")
+ .mockImplementation(() => ({
+ id: "39cc62ab-1df0-4a9d-974d-4c58173a1750",
+ is_revoked: true,
+ revocation_reason: "REVOKED_BY_USER"
+ }));
+
+ renderComponent(WalletCardsContainer);
+
+ expect(Alert.alert).toHaveBeenCalledWith(
+ I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.revokedByUser.title"
+ ),
+ I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.revokedByUser.content"
+ ),
+ [
+ {
+ text: I18n.t(
+ "features.itWallet.walletInstanceRevoked.alert.closeButtonAlt"
+ )
+ }
+ ]
+ );
+ });
});
const renderComponent = (component: React.ComponentType) => {
diff --git a/ts/features/wallet/components/__tests__/WalletCategoryFilterTabs.test.tsx b/ts/features/wallet/components/__tests__/WalletCategoryFilterTabs.test.tsx
index 06452f7ab0c..9740c683227 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 if category filtering is not enabled", () => {
jest
.spyOn(selectors, "selectWalletCategoryFilter")
.mockImplementation(() => undefined);
jest
- .spyOn(selectors, "selectWalletCategories")
- .mockImplementation(() => new Set(["itw"]));
+ .spyOn(selectors, "isWalletCategoryFilteringEnabledSelector")
+ .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 if category filtering is enabled", () => {
jest
.spyOn(selectors, "selectWalletCategoryFilter")
.mockImplementation(() => undefined);
jest
- .spyOn(selectors, "selectWalletCategories")
- .mockImplementation(() => new Set(["itw", "other"]));
+ .spyOn(selectors, "isWalletCategoryFilteringEnabledSelector")
+ .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, "isWalletCategoryFilteringEnabledSelector")
+ .mockImplementation(() => true);
const { getByTestId } = renderComponent();
const itwTab = getByTestId("CategoryTabTestID-itw");
diff --git a/ts/features/wallet/saga/index.ts b/ts/features/wallet/saga/index.ts
index 5511df75f9b..36eb1639359 100644
--- a/ts/features/wallet/saga/index.ts
+++ b/ts/features/wallet/saga/index.ts
@@ -10,7 +10,7 @@ import {
import { walletUpdate } from "../store/actions";
import { walletAddCards } from "../store/actions/cards";
import { walletToggleLoadingState } from "../store/actions/placeholders";
-import { selectWalletPlaceholders } from "../store/selectors";
+import { selectWalletPlaceholderCards } from "../store/selectors";
import { handleWalletAnalyticsSaga } from "./handleWalletAnalyticsSaga";
import { handleWalletPlaceholdersTimeout } from "./handleWalletLoadingPlaceholdersTimeout";
import { handleWalletLoadingStateSaga } from "./handleWalletLoadingStateSaga";
@@ -21,7 +21,7 @@ const LOADING_STATE_TIMEOUT = 2000 as Millisecond;
export function* watchWalletSaga(): SagaIterator {
// Adds persisted placeholders as cards in the wallet
// to be displayed while waiting for the actual cards
- const placeholders = yield* select(selectWalletPlaceholders);
+ const placeholders = yield* select(selectWalletPlaceholderCards);
yield* put(walletAddCards(placeholders));
yield* takeLatest(
diff --git a/ts/features/wallet/screens/__tests__/WalletHomeScreen.test.tsx b/ts/features/wallet/screens/__tests__/WalletHomeScreen.test.tsx
index 6bae047df8c..048de392d91 100644
--- a/ts/features/wallet/screens/__tests__/WalletHomeScreen.test.tsx
+++ b/ts/features/wallet/screens/__tests__/WalletHomeScreen.test.tsx
@@ -1,12 +1,10 @@
-import * as pot from "@pagopa/ts-commons/lib/pot";
-import _ from "lodash";
import configureMockStore from "redux-mock-store";
import ROUTES from "../../../../navigation/routes";
import { applicationChangeState } from "../../../../store/actions/application";
import { appReducer } from "../../../../store/reducers";
import { GlobalState } from "../../../../store/reducers/types";
import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper";
-import { WalletCardsState } from "../../store/reducers/cards";
+import * as walletSelectors from "../../store/selectors";
import { WalletHomeScreen } from "../WalletHomeScreen";
jest.mock("react-native-reanimated", () => ({
@@ -18,98 +16,45 @@ jest.mock("react-native-reanimated", () => ({
}
}));
-const T_CARDS: WalletCardsState = {
- "1": {
- key: "1",
- type: "payment",
- category: "payment",
- walletId: ""
- },
- "2": {
- key: "2",
- type: "payment",
- category: "payment",
- walletId: ""
- },
- "3": {
- key: "3",
- type: "idPay",
- category: "bonus",
- amount: 1234,
- avatarSource: {
- uri: ""
- },
- expireDate: new Date(),
- initiativeId: "",
- name: "ABC"
- }
-};
-
describe("WalletHomeScreen", () => {
jest.useFakeTimers();
jest.runAllTimers();
- it("should correctly render empty screen", () => {
- const {
- component: { queryByTestId }
- } = renderComponent({});
+ it("should not render screen actions if the wallet is empty", () => {
+ jest
+ .spyOn(walletSelectors, "isWalletEmptySelector")
+ .mockImplementation(() => true);
+
+ const { queryByTestId } = renderComponent();
jest.runOnlyPendingTimers();
- expect(queryByTestId("walletPaymentsRedirectBannerTestID")).toBeNull();
- expect(queryByTestId("walletEmptyScreenContentTestID")).not.toBeNull();
- expect(queryByTestId("walletCardsContainerTestID")).toBeNull();
expect(queryByTestId("walletAddCardButtonTestID")).toBeNull();
});
- it("should correctly render card list screen", () => {
- const {
- component: { queryByTestId }
- } = renderComponent(T_CARDS);
+ it("should render screen actions if the wallet is not empty", () => {
+ jest
+ .spyOn(walletSelectors, "isWalletEmptySelector")
+ .mockImplementation(() => false);
+
+ const { queryByTestId } = renderComponent();
+
+ jest.runOnlyPendingTimers();
- expect(queryByTestId("walletPaymentsRedirectBannerTestID")).toBeNull();
- expect(queryByTestId("walletEmptyScreenContentTestID")).toBeNull();
- expect(queryByTestId("walletCardsContainerTestID")).not.toBeNull();
expect(queryByTestId("walletAddCardButtonTestID")).not.toBeNull();
});
});
-const renderComponent = (
- cards: WalletCardsState,
- options: {
- isLoading?: boolean;
- } = {}
-) => {
+const renderComponent = () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
- const { isLoading = false } = options;
const mockStore = configureMockStore();
- const store: ReturnType = mockStore(
- _.merge(globalState, {
- features: {
- wallet: {
- cards,
- preferences: {},
- placeholders: {
- isLoading
- }
- },
- payments: {
- wallet: {
- userMethods: pot.some([])
- }
- }
- }
- })
- );
+ const store: ReturnType = mockStore(globalState);
- return {
- component: renderScreenWithNavigationStoreContext(
- WalletHomeScreen,
- ROUTES.WALLET_HOME,
- {},
- store
- ),
+ return renderScreenWithNavigationStoreContext(
+ WalletHomeScreen,
+ ROUTES.WALLET_HOME,
+ {},
store
- };
+ );
};
diff --git a/ts/features/wallet/store/reducers/preferences.ts b/ts/features/wallet/store/reducers/preferences.ts
index 49cf5f2c708..01b39b1dc87 100644
--- a/ts/features/wallet/store/reducers/preferences.ts
+++ b/ts/features/wallet/store/reducers/preferences.ts
@@ -1,9 +1,17 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
-import { PersistConfig, persistReducer } from "redux-persist";
+import _ from "lodash";
+import {
+ MigrationManifest,
+ PersistConfig,
+ PersistedState,
+ createMigrate,
+ persistReducer
+} from "redux-persist";
import { getType } from "typesafe-actions";
import { Action } from "../../../../store/actions/types";
-import { walletSetCategoryFilter } from "../actions/preferences";
+import { isDevEnv } from "../../../../utils/environment";
import { WalletCardCategoryFilter } from "../../types";
+import { walletSetCategoryFilter } from "../actions/preferences";
export type WalletPreferencesState = {
categoryFilter?: WalletCardCategoryFilter;
@@ -25,12 +33,20 @@ const reducer = (
return state;
};
-const CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION = -1;
+const CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION = 0;
+
+const migrations: MigrationManifest = {
+ // Removed categoryFilter persistance requirement
+ "0": (state: PersistedState): PersistedState =>
+ _.set(state, "preferences", {})
+};
const persistConfig: PersistConfig = {
key: "walletPreferences",
storage: AsyncStorage,
- version: CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION
+ version: CURRENT_REDUX_WALLET_PREFERENCES_STORE_VERSION,
+ blacklist: ["categoryFilter"],
+ migrate: createMigrate(migrations, { debug: isDevEnv })
};
export const walletReducerPersistor = persistReducer<
diff --git a/ts/features/wallet/store/selectors/__tests__/index.test.ts b/ts/features/wallet/store/selectors/__tests__/index.test.ts
index bf524fdbf2d..c1030f3dc6d 100644
--- a/ts/features/wallet/store/selectors/__tests__/index.test.ts
+++ b/ts/features/wallet/store/selectors/__tests__/index.test.ts
@@ -1,10 +1,14 @@
-import * as O from "fp-ts/lib/Option";
import * as pot from "@pagopa/ts-commons/lib/pot";
+import * as O from "fp-ts/lib/Option";
import _ from "lodash";
import {
+ isWalletCategoryFilteringEnabledSelector,
isWalletEmptySelector,
selectWalletCards,
+ selectWalletCardsByCategory,
+ selectWalletCardsByType,
selectWalletCategories,
+ shouldRenderWalletCategorySelector,
shouldRenderWalletEmptyStateSelector
} from "..";
import { applicationChangeState } from "../../../../../store/actions/application";
@@ -15,9 +19,25 @@ import {
} from "../../../../itwallet/common/utils/itwMocksUtils";
import { ItwLifecycleState } from "../../../../itwallet/lifecycle/store/reducers";
import * as itwLifecycleSelectors from "../../../../itwallet/lifecycle/store/selectors";
+import { walletCardCategoryFilters } from "../../../types";
import { WalletCardsState } from "../../reducers/cards";
-const T_CARDS: WalletCardsState = {
+const T_ITW_CARDS: WalletCardsState = {
+ "4": {
+ key: "4",
+ category: "itw",
+ type: "itw",
+ credentialType: CredentialType.DRIVING_LICENSE
+ },
+ "5": {
+ key: "5",
+ category: "itw",
+ type: "itw",
+ credentialType: CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD
+ }
+};
+
+const T_OTHER_CARDS: WalletCardsState = {
"1": {
key: "1",
category: "payment",
@@ -40,21 +60,14 @@ const T_CARDS: WalletCardsState = {
key: "3",
category: "cgn",
type: "cgn"
- },
- "4": {
- key: "4",
- category: "itw",
- type: "itw",
- credentialType: CredentialType.DRIVING_LICENSE
- },
- "5": {
- key: "5",
- category: "itw",
- type: "itw",
- credentialType: CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD
}
};
+const T_CARDS: WalletCardsState = {
+ ...T_ITW_CARDS,
+ ...T_OTHER_CARDS
+};
+
describe("selectWalletCards", () => {
it("should return the correct cards", () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
@@ -128,6 +141,34 @@ describe("selectWalletCategories", () => {
});
});
+describe("selectWalletCardsByType", () => {
+ it("should return the correct cards", () => {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+
+ const cards = selectWalletCardsByType(
+ _.set(globalState, "features.wallet", {
+ cards: T_CARDS
+ }),
+ "idPay"
+ );
+ expect(cards).toEqual([T_CARDS["2"]]);
+ });
+});
+
+describe("selectWalletCardsByCategory", () => {
+ it("should return the correct cards", () => {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+
+ const cards = selectWalletCardsByCategory(
+ _.set(globalState, "features.wallet", {
+ cards: T_CARDS
+ }),
+ "itw"
+ );
+ expect(cards).toEqual([T_CARDS["4"], T_CARDS["5"]]);
+ });
+});
+
describe("isWalletEmptySelector", () => {
it("should return true if there are no categories to display", () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
@@ -220,3 +261,108 @@ describe("shouldRenderWalletEmptyStateSelector", () => {
}
);
});
+
+describe("isWalletCategoryFilteringEnabledSelector", () => {
+ it("should return true if the categories are ['itw', 'other']", () => {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+
+ const isWalletCategoryFilteringEnabled =
+ isWalletCategoryFilteringEnabledSelector(
+ _.merge(
+ globalState,
+ _.set(globalState, "features.wallet", {
+ cards: T_CARDS
+ })
+ )
+ );
+
+ expect(isWalletCategoryFilteringEnabled).toBe(true);
+ });
+
+ it("should return false if the categories are ['itw']", () => {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+
+ const isWalletCategoryFilteringEnabled =
+ isWalletCategoryFilteringEnabledSelector(
+ _.merge(
+ globalState,
+ _.set(globalState, "features.wallet", {
+ cards: T_ITW_CARDS
+ })
+ )
+ );
+
+ expect(isWalletCategoryFilteringEnabled).toBe(false);
+ });
+});
+
+describe("shouldRenderWalletCategorySelector", () => {
+ it("should return true if the category filter is undefined", () => {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+
+ const shouldRenderWalletCategory = shouldRenderWalletCategorySelector(
+ _.merge(
+ globalState,
+ _.set(globalState, "features.wallet", {
+ cards: T_CARDS,
+ preferences: {
+ categoryFilter: undefined
+ }
+ })
+ ),
+ "itw"
+ );
+
+ expect(shouldRenderWalletCategory).toBe(true);
+ });
+
+ it.each(walletCardCategoryFilters)(
+ "should return true if the category filter matches the given category when the category is %s",
+ categoryFilter => {
+ const globalState = appReducer(
+ undefined,
+ applicationChangeState("active")
+ );
+
+ const shouldRenderWalletCategory = shouldRenderWalletCategorySelector(
+ _.merge(
+ globalState,
+ _.set(globalState, "features.wallet", {
+ cards: T_CARDS,
+ preferences: {
+ categoryFilter
+ }
+ })
+ ),
+ categoryFilter
+ );
+
+ expect(shouldRenderWalletCategory).toBe(true);
+ }
+ );
+
+ it.each(walletCardCategoryFilters)(
+ "should return true if the category filtering is not enabled and the category filter is %s",
+ categoryFilter => {
+ const globalState = appReducer(
+ undefined,
+ applicationChangeState("active")
+ );
+
+ const shouldRenderWalletCategory = shouldRenderWalletCategorySelector(
+ _.merge(
+ globalState,
+ _.set(globalState, "features.wallet", {
+ cards: T_ITW_CARDS,
+ preferences: {
+ categoryFilter
+ }
+ })
+ ),
+ categoryFilter
+ );
+
+ expect(shouldRenderWalletCategory).toBe(true);
+ }
+ );
+});
diff --git a/ts/features/wallet/store/selectors/index.ts b/ts/features/wallet/store/selectors/index.ts
index 0fc70d8a3fd..5ee212b0cce 100644
--- a/ts/features/wallet/store/selectors/index.ts
+++ b/ts/features/wallet/store/selectors/index.ts
@@ -1,35 +1,31 @@
import * as pot from "@pagopa/ts-commons/lib/pot";
import { createSelector } from "reselect";
import { GlobalState } from "../../../../store/reducers/types";
+import { isSomeLoadingOrSomeUpdating } from "../../../../utils/pot";
import { cgnDetailSelector } from "../../../bonus/cgn/store/reducers/details";
import { idPayWalletInitiativeListSelector } from "../../../idpay/wallet/store/reducers";
import { itwLifecycleIsValidSelector } from "../../../itwallet/lifecycle/store/selectors";
import { paymentsWalletUserMethodsSelector } from "../../../payments/wallet/store/selectors";
-import { WalletCard, walletCardCategories } from "../../types";
-import { isSomeLoadingOrSomeUpdating } from "../../../../utils/pot";
-
-const selectWalletFeature = (state: GlobalState) => state.features.wallet;
-
-export const selectWalletPlaceholders = createSelector(
- selectWalletFeature,
- wallet =>
- Object.entries(wallet.placeholders.items).map(
- ([key, category]) =>
- ({ key, category, type: "placeholder" } as WalletCard)
- )
-);
+import {
+ WalletCard,
+ WalletCardCategory,
+ WalletCardType,
+ walletCardCategories
+} from "../../types";
+import { WalletCardCategoryFilter } from "../../types/index";
/**
* Returns the list of cards excluding hidden cards
*/
export const selectWalletCards = createSelector(
- selectWalletFeature,
- ({ cards }) => Object.values(cards).filter(({ hidden }) => !hidden)
+ (state: GlobalState) => state.features.wallet.cards,
+ cards => Object.values(cards).filter(({ hidden }) => !hidden)
);
/**
* Returns the list of card categories available in the wallet
* If there are categories other that ITW, they will become "other"
+ * If the ITW is valid, it will be counted as "itw" category, since we do not have eID card anymore
*/
export const selectWalletCategories = createSelector(
selectWalletCards,
@@ -66,47 +62,58 @@ export const selectSortedWalletCards = createSelector(
);
/**
- * Only gets cards which are part of the IT Wallet
+ * Selects the cards by their category
+ * @param category - The category of the cards to select
*/
-export const selectWalletItwCards = createSelector(
+export const selectWalletCardsByCategory = createSelector(
selectSortedWalletCards,
- cards => cards.filter(({ category }) => category === "itw")
+ (_: GlobalState, category: WalletCardCategory) => category,
+ (cards, category) =>
+ cards.filter(({ category: cardCategory }) => cardCategory === category)
);
/**
- * Only gets cards which are not part of the IT Wallet
+ * Selects the cards by their type
+ * @param type - The type of the cards to select
*/
-export const selectWalletOtherCards = createSelector(
+export const selectWalletCardsByType = createSelector(
selectSortedWalletCards,
- cards => cards.filter(({ category }) => category !== "itw")
+ (_: GlobalState, type: WalletCardType) => type,
+ (cards, type) => cards.filter(({ type: cardType }) => cardType === type)
);
-export const selectIsWalletCardsLoading = (state: GlobalState) =>
- state.features.wallet.placeholders.isLoading;
-
-export const selectWalletCategoryFilter = createSelector(
- selectWalletFeature,
- wallet => wallet.preferences.categoryFilter
-);
-
-export const selectWalletPaymentMethods = createSelector(
+/**
+ * Currently, if a card is not part of the IT Wallet, it is considered as "other"
+ * This selector returns the cards which are not part of the IT Wallet which must be displayed in the "other" section
+ */
+export const selectWalletOtherCards = createSelector(
selectSortedWalletCards,
- cards => cards.filter(({ category }) => category === "payment")
+ cards => cards.filter(({ category }) => category !== "itw")
);
-export const selectWalletCgnCard = createSelector(
- selectSortedWalletCards,
- cards => cards.filter(({ category }) => category === "cgn")
-);
+/**
+ * Selects the loading state of the wallet cards
+ */
+export const selectIsWalletLoading = (state: GlobalState) =>
+ state.features.wallet.placeholders.isLoading;
-export const selectBonusCards = createSelector(selectSortedWalletCards, cards =>
- cards.filter(({ category }) => category === "bonus")
+/**
+ * Selects the placeholders from the wallet
+ */
+export const selectWalletPlaceholderCards = createSelector(
+ (state: GlobalState) => state.features.wallet.placeholders.items,
+ placeholders =>
+ Object.entries(placeholders).map(
+ ([key, category]) =>
+ ({ key, category, type: "placeholder" } as WalletCard)
+ )
);
/**
* Gets if the wallet can be considered empty.
- * The wallet is empty if there are no categories to display
- * @see selectWalletCategories
+ * The wallet is empty if there are no categories to display (@see selectWalletCategories)
+ *
+ * Note: we check categories because if ITW is valid, it is considered as one category even if there are no cards
*/
export const isWalletEmptySelector = (state: GlobalState) =>
selectWalletCategories(state).size === 0;
@@ -127,3 +134,32 @@ export const isWalletScreenRefreshingSelector = (state: GlobalState) =>
isSomeLoadingOrSomeUpdating(paymentsWalletUserMethodsSelector(state)) ||
isSomeLoadingOrSomeUpdating(idPayWalletInitiativeListSelector(state)) ||
isSomeLoadingOrSomeUpdating(cgnDetailSelector(state));
+
+/**
+ * Selects if the wallet categories can be filtered.
+ * The filter is only enabled if there are more than one category available
+ */
+export const isWalletCategoryFilteringEnabledSelector = createSelector(
+ selectWalletCategories,
+ categories => categories.size > 1
+);
+
+/**
+ * Selects the category filter from the wallet preferences
+ */
+export const selectWalletCategoryFilter = (state: GlobalState) =>
+ state.features.wallet.preferences.categoryFilter;
+
+/**
+ * Checks if a wallet category section should be rendered. A category section is rendered if:
+ * - the category filtering is not enabled, or
+ * - no category filter is selected, or
+ * - the filter matches the given category
+ */
+export const shouldRenderWalletCategorySelector = createSelector(
+ isWalletCategoryFilteringEnabledSelector,
+ selectWalletCategoryFilter,
+ (_: GlobalState, category: WalletCardCategoryFilter) => category,
+ (isFilteringEnabled, filter, category) =>
+ !isFilteringEnabled || filter === undefined || filter === category
+);
diff --git a/ts/features/wallet/utils/__tests__/index.test.tsx b/ts/features/wallet/utils/__tests__/index.test.tsx
new file mode 100644
index 00000000000..534d6e66d93
--- /dev/null
+++ b/ts/features/wallet/utils/__tests__/index.test.tsx
@@ -0,0 +1,40 @@
+import { Body } from "@pagopa/io-app-design-system";
+import * as React from "react";
+import configureMockStore from "redux-mock-store";
+import { withWalletCategoryFilter } from "..";
+import ROUTES from "../../../../navigation/routes";
+import { applicationChangeState } from "../../../../store/actions/application";
+import { appReducer } from "../../../../store/reducers";
+import { GlobalState } from "../../../../store/reducers/types";
+import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper";
+import * as selectors from "../../store/selectors";
+
+describe("withWalletCategoryFilter", () => {
+ it("should return null if the category filter does not match", () => {
+ const WrappedComponent = () => (
+ Hello
+ );
+ const ComponentWithFilter = withWalletCategoryFilter(
+ "itw",
+ WrappedComponent
+ );
+
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+
+ const mockStore = configureMockStore();
+ const store: ReturnType = mockStore(globalState);
+
+ jest
+ .spyOn(selectors, "shouldRenderWalletCategorySelector")
+ .mockImplementation(() => false);
+
+ const { queryByTestId } =
+ renderScreenWithNavigationStoreContext(
+ () => ,
+ ROUTES.WALLET_HOME,
+ {},
+ store
+ );
+ expect(queryByTestId("WrappedComponentTestID")).toBeNull();
+ });
+});
diff --git a/ts/features/wallet/utils/index.tsx b/ts/features/wallet/utils/index.tsx
index e2dad27dfdd..00a11ed90fe 100644
--- a/ts/features/wallet/utils/index.tsx
+++ b/ts/features/wallet/utils/index.tsx
@@ -1,11 +1,14 @@
import * as React from "react";
-import { WalletCard, WalletCardType } from "../types";
+import { WalletCard, WalletCardCategoryFilter, WalletCardType } from "../types";
import { WalletCardBaseComponent } from "../components/WalletCardBaseComponent";
import { CgnWalletCard } from "../../bonus/cgn/components/CgnWalletCard";
import { IdPayWalletCard } from "../../idpay/wallet/components/IdPayWalletCard";
import { PaymentWalletCard } from "../../payments/wallet/components/PaymentWalletCard";
import { WalletCardSkeleton } from "../components/WalletCardSkeleton";
import { ItwCredentialWalletCard } from "../../itwallet/wallet/components/ItwCredentialWalletCard";
+import { shouldRenderWalletCategorySelector } from "../store/selectors";
+import { useIOSelector } from "../../../store/hooks";
+import { GlobalState } from "../../../store/reducers/types";
/**
* Wallet card component mapper which translates a WalletCardType to a
@@ -24,6 +27,12 @@ export const walletCardComponentMapper: Record<
placeholder: WalletCardSkeleton
};
+/**
+ * Function that renders a wallet card using the mapped component inside {@see walletCardComponentMapper}
+ * @param card - The wallet card object to render
+ * @param stacked - Whether the card is stacked or not
+ * @returns The rendered card or null if the card is not found
+ */
export const renderWalletCardFn = (
card: WalletCard,
stacked: boolean = false
@@ -39,3 +48,24 @@ export const renderWalletCardFn = (
/>
) : null;
};
+
+/**
+ * A higher-order component which renders a component only if the category filter matches the given category
+ * @param category - The category to filter by
+ * @param WrappedComponent - The component to render
+ * @returns The component or null if the category filter does not match
+ */
+export const withWalletCategoryFilter =
+ >(
+ category: WalletCardCategoryFilter,
+ WrappedComponent: React.ComponentType
+ ) =>
+ (props: P) => {
+ const shouldRenderCategory = useIOSelector((state: GlobalState) =>
+ shouldRenderWalletCategorySelector(state, category)
+ );
+ if (!shouldRenderCategory) {
+ return null;
+ }
+ return ;
+ };
diff --git a/ts/mixpanelConfig/mixpanelPropertyUtils.ts b/ts/mixpanelConfig/mixpanelPropertyUtils.ts
index 1d0617f2898..19b3fbe8ed8 100644
--- a/ts/mixpanelConfig/mixpanelPropertyUtils.ts
+++ b/ts/mixpanelConfig/mixpanelPropertyUtils.ts
@@ -4,10 +4,7 @@ import { ServicesPreferencesModeEnum } from "../../definitions/backend/ServicesP
import { TrackCgnStatus } from "../features/bonus/cgn/analytics";
import { LoginSessionDuration } from "../features/fastLogin/analytics/optinAnalytics";
import { fastLoginOptInSelector } from "../features/fastLogin/store/selectors";
-import {
- selectBonusCards,
- selectWalletCgnCard
-} from "../features/wallet/store/selectors";
+import { selectWalletCardsByType } from "../features/wallet/store/selectors";
import { WalletCardBonus } from "../features/wallet/types";
import { paymentsWalletUserMethodsSelector } from "../features/payments/wallet/store/selectors";
import {
@@ -91,17 +88,16 @@ export const paymentMethodsHandler = (state: GlobalState): number | undefined =>
paymentsWalletUserMethodsNumberFromPotSelector(state)?.length;
export const cgnStatusHandler = (state: GlobalState): TrackCgnStatus => {
- const cgnCard = selectWalletCgnCard(state);
+ const cgnCard = selectWalletCardsByType(state, "cgn");
return cgnCard.length > 0 ? "active" : "not_active";
};
export const welfareStatusHandler = (
state: GlobalState
): ReadonlyArray => {
- const bonusCards = selectBonusCards(state);
- const idPayCards = bonusCards.filter(
- card => card.type === "idPay"
+ const idPayCards = selectWalletCardsByType(
+ state,
+ "idPay"
) as Array;
-
return idPayCards.map(card => card.name);
};
diff --git a/ts/store/reducers/debug.ts b/ts/store/reducers/debug.ts
index 79b127ba1ac..99c957ee9a4 100644
--- a/ts/store/reducers/debug.ts
+++ b/ts/store/reducers/debug.ts
@@ -1,4 +1,5 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
+import _ from "lodash";
import { PersistConfig, PersistPartial, persistReducer } from "redux-persist";
import { getType } from "typesafe-actions";
import {
@@ -37,7 +38,7 @@ function debugReducer(
case getType(setDebugData):
return {
...state,
- debugData: action.payload
+ debugData: _.merge(state.debugData, action.payload)
};
case getType(resetDebugData):
return {