diff --git a/ts/features/itwallet/common/utils/itwCredentialStatusAttestationUtils.ts b/ts/features/itwallet/common/utils/itwCredentialStatusAttestationUtils.ts
index df51ad080d5..5892d46f340 100644
--- a/ts/features/itwallet/common/utils/itwCredentialStatusAttestationUtils.ts
+++ b/ts/features/itwallet/common/utils/itwCredentialStatusAttestationUtils.ts
@@ -3,6 +3,7 @@ import {
Credential
} from "@pagopa/io-react-native-wallet";
import { isAfter } from "date-fns";
+import * as t from "io-ts";
import { StoredCredential } from "./itwTypesUtils";
export const getCredentialStatusAttestation = async (
@@ -54,3 +55,11 @@ export const shouldRequestStatusAttestation = ({
throw new Error("Unexpected credential status");
}
};
+
+/**
+ * Shape of a credential status attestation response error.
+ */
+export const StatusAttestationError = t.intersection([
+ t.type({ error: t.string }),
+ t.partial({ error_description: t.string })
+]);
diff --git a/ts/features/itwallet/credentials/saga/checkCredentialsStatusAttestation.ts b/ts/features/itwallet/credentials/saga/checkCredentialsStatusAttestation.ts
index 9d795468ea5..cf896a843bd 100644
--- a/ts/features/itwallet/credentials/saga/checkCredentialsStatusAttestation.ts
+++ b/ts/features/itwallet/credentials/saga/checkCredentialsStatusAttestation.ts
@@ -7,7 +7,8 @@ import { itwCredentialsSelector } from "../store/selectors";
import { StoredCredential } from "../../common/utils/itwTypesUtils";
import {
shouldRequestStatusAttestation,
- getCredentialStatusAttestation
+ getCredentialStatusAttestation,
+ StatusAttestationError
} from "../../common/utils/itwCredentialStatusAttestationUtils";
import { ReduxSagaEffect } from "../../../../types/utils";
import { itwLifecycleIsValidSelector } from "../../lifecycle/store/selectors";
@@ -15,7 +16,6 @@ import { itwCredentialsStore } from "../store/actions";
import { updateMixpanelProfileProperties } from "../../../../mixpanelConfig/profileProperties";
import { updateMixpanelSuperProperties } from "../../../../mixpanelConfig/superProperties";
import { GlobalState } from "../../../../store/reducers/types";
-import { StatusAttestationError } from "./types";
const { isIssuerResponseError, IssuerResponseErrorCodes: Codes } = Errors;
diff --git a/ts/features/itwallet/credentials/saga/types.ts b/ts/features/itwallet/credentials/saga/types.ts
deleted file mode 100644
index b63c9adef8f..00000000000
--- a/ts/features/itwallet/credentials/saga/types.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as t from "io-ts";
-
-export const StatusAttestationError = t.type({
- error: t.string
-});
diff --git a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialFailureScreen.tsx b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialFailureScreen.tsx
index b976239371c..dd7a4b39a66 100644
--- a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialFailureScreen.tsx
+++ b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialFailureScreen.tsx
@@ -11,9 +11,7 @@ import { useDebugInfo } from "../../../../hooks/useDebugInfo";
import I18n from "../../../../i18n";
import {
CredentialIssuanceFailure,
- CredentialIssuanceFailureType,
- CredentialIssuanceFailureTypeEnum,
- isCredentialInvalidStatusError
+ CredentialIssuanceFailureType
} from "../../machine/credential/failure";
import {
selectCredentialTypeOption,
@@ -26,7 +24,6 @@ import { useAvoidHardwareBackButton } from "../../../../utils/useAvoidHardwareBa
import {
CREDENTIALS_MAP,
trackAddCredentialFailure,
- trackAddCredentialTimeout,
trackCredentialInvalidStatusFailure,
trackCredentialNotEntitledFailure,
trackItWalletDeferredIssuing,
@@ -35,6 +32,7 @@ import {
import ROUTES from "../../../../navigation/routes";
import { MESSAGES_ROUTES } from "../../../messages/navigation/routes";
import { getClaimsFullLocale } from "../../common/utils/itwClaimsUtils";
+import { StatusAttestationError } from "../../common/utils/itwCredentialStatusAttestationUtils";
export const ItwIssuanceCredentialFailureScreen = () => {
const failureOption =
@@ -60,7 +58,7 @@ const ContentView = ({ failure }: ContentViewProps) => {
selectCredentialTypeOption
);
- const invalidStatusMessage = useCredentialInvalidStatusMessage(failure);
+ const invalidStatusDetails = useCredentialInvalidStatusDetails(failure);
const closeIssuance = (cta_id: string) => {
machineRef.send({ type: "close" });
@@ -81,107 +79,130 @@ const ContentView = ({ failure }: ContentViewProps) => {
failure
});
- const resultScreensMap: Record<
- CredentialIssuanceFailureType,
- OperationResultScreenContentProps
- > = {
- GENERIC: {
- title: I18n.t("features.itWallet.issuance.genericError.title"),
- subtitle: I18n.t("features.itWallet.issuance.genericError.body"),
- pictogram: "umbrellaNew",
- action: {
- label: I18n.t("features.itWallet.issuance.genericError.primaryAction"),
- onPress: () =>
- closeIssuance(
- I18n.t("features.itWallet.issuance.genericError.primaryAction")
- )
+ const getOperationResultScreenContentProps =
+ (): OperationResultScreenContentProps => {
+ switch (failure.type) {
+ case CredentialIssuanceFailureType.UNEXPECTED:
+ case CredentialIssuanceFailureType.ISSUER_GENERIC:
+ case CredentialIssuanceFailureType.WALLET_PROVIDER_GENERIC:
+ return {
+ title: I18n.t("features.itWallet.issuance.genericError.title"),
+ subtitle: I18n.t("features.itWallet.issuance.genericError.body"),
+ pictogram: "umbrellaNew",
+ action: {
+ label: I18n.t(
+ "features.itWallet.issuance.genericError.primaryAction"
+ ),
+ onPress: () =>
+ closeIssuance(
+ I18n.t(
+ "features.itWallet.issuance.genericError.primaryAction"
+ )
+ )
+ }
+ };
+ // NOTE: only the mDL supports the async flow, so this error message is specific to mDL
+ case CredentialIssuanceFailureType.ASYNC_ISSUANCE:
+ return {
+ title: I18n.t(
+ "features.itWallet.issuance.asyncCredentialError.title"
+ ),
+ subtitle: I18n.t(
+ "features.itWallet.issuance.asyncCredentialError.body"
+ ),
+ pictogram: "pending",
+ action: {
+ label: I18n.t(
+ "features.itWallet.issuance.asyncCredentialError.primaryAction"
+ ),
+ onPress: closeAsyncIssuance
+ }
+ };
+ // Dynamic errors extracted from the entity configuration, with fallback
+ case CredentialIssuanceFailureType.INVALID_STATUS:
+ return {
+ title:
+ invalidStatusDetails.message?.title ??
+ defaultInvalidStatusMessage.title,
+ subtitle:
+ invalidStatusDetails.message?.description ??
+ defaultInvalidStatusMessage.description,
+ pictogram: "accessDenied",
+ action: {
+ label: I18n.t(
+ "features.itWallet.issuance.notEntitledCredentialError.primaryAction"
+ ),
+ onPress: () =>
+ closeIssuance(
+ I18n.t(
+ "features.itWallet.issuance.notEntitledCredentialError.primaryAction"
+ )
+ )
+ }
+ };
}
- },
- // NOTE: only the mDL supports the async flow, so this error message is specific to mDL
- ASYNC_ISSUANCE: {
- title: I18n.t("features.itWallet.issuance.asyncCredentialError.title"),
- subtitle: I18n.t("features.itWallet.issuance.asyncCredentialError.body"),
- pictogram: "pending",
- action: {
- label: I18n.t(
- "features.itWallet.issuance.asyncCredentialError.primaryAction"
- ),
- onPress: closeAsyncIssuance
- }
- },
- // Dynamic errors extracted from the entity configuration
- INVALID_STATUS: {
- title:
- invalidStatusMessage?.title ??
- I18n.t("features.itWallet.issuance.notEntitledCredentialError.title"),
- subtitle:
- invalidStatusMessage?.description ??
- I18n.t("features.itWallet.issuance.notEntitledCredentialError.body"),
- pictogram: "accessDenied",
- action: {
- label: I18n.t(
- "features.itWallet.issuance.notEntitledCredentialError.primaryAction"
- ),
- onPress: () =>
- closeIssuance(
- I18n.t(
- "features.itWallet.issuance.notEntitledCredentialError.primaryAction"
- )
- )
- }
- }
- };
+ };
useEffect(() => {
if (O.isNone(credentialType)) {
return;
}
- if (failure.type === CredentialIssuanceFailureTypeEnum.ASYNC_ISSUANCE) {
+ if (failure.type === CredentialIssuanceFailureType.ASYNC_ISSUANCE) {
trackItWalletDeferredIssuing(CREDENTIALS_MAP[credentialType.value]);
return;
}
- if (failure.type === CredentialIssuanceFailureTypeEnum.INVALID_STATUS) {
- const error = failure.reason as Errors.CredentialInvalidStatusError;
-
+ if (failure.type === CredentialIssuanceFailureType.INVALID_STATUS) {
const trackingFunction =
- error.errorCode === "credential_not_found"
+ invalidStatusDetails.errorCode === "credential_not_found"
? trackCredentialNotEntitledFailure
: trackCredentialInvalidStatusFailure;
trackingFunction({
- reason: error.errorCode,
+ reason: invalidStatusDetails.errorCode,
type: failure.type,
credential: CREDENTIALS_MAP[credentialType.value]
});
return;
}
- if (failure.type === CredentialIssuanceFailureTypeEnum.GENERIC) {
+ if (
+ failure.type === CredentialIssuanceFailureType.UNEXPECTED ||
+ failure.type === CredentialIssuanceFailureType.ISSUER_GENERIC ||
+ failure.type === CredentialIssuanceFailureType.WALLET_PROVIDER_GENERIC
+ ) {
trackAddCredentialFailure({
reason: failure.reason,
type: failure.type,
credential: CREDENTIALS_MAP[credentialType.value]
});
- return;
}
- trackAddCredentialTimeout({
+
+ /* trackAddCredentialTimeout({
reason: failure.reason,
type: failure.type,
credential: CREDENTIALS_MAP[credentialType.value]
- });
- }, [credentialType, failure]);
+ }); */
+ }, [credentialType, failure, invalidStatusDetails.errorCode]);
- const resultScreenProps = resultScreensMap[failure.type];
+ const resultScreenProps = getOperationResultScreenContentProps();
return ;
};
+const defaultInvalidStatusMessage = {
+ title: I18n.t("features.itWallet.issuance.notEntitledCredentialError.title"),
+ description: I18n.t(
+ "features.itWallet.issuance.notEntitledCredentialError.body"
+ )
+};
+
/**
- * Hook used to safely extract the localized message from an invalid status error.
- * This message is dynamic and must be extracted from the EC.
+ * Hook used to safely extract details from an invalid status error, including the localized message.
+ *
+ * **Note:** The message is dynamic and must be extracted from the EC.
*/
-const useCredentialInvalidStatusMessage = (
+const useCredentialInvalidStatusDetails = (
failure: CredentialIssuanceFailure
) => {
const credentialType = ItwCredentialIssuanceMachineContext.useSelector(
@@ -191,17 +212,30 @@ const useCredentialInvalidStatusMessage = (
selectIssuerConfigurationOption
);
- return pipe(
+ const errorCodeOption = pipe(
+ failure,
+ O.fromPredicate(
+ e => e.type === CredentialIssuanceFailureType.INVALID_STATUS
+ ),
+ O.chainEitherK(e => StatusAttestationError.decode(e.reason.reason)),
+ O.map(x => x.error)
+ );
+
+ const localizedMessage = pipe(
sequenceS(O.Monad)({
- failure: pipe(failure, O.fromPredicate(isCredentialInvalidStatusError)),
+ errorCode: errorCodeOption,
credentialType,
issuerConf
}),
- // eslint-disable-next-line @typescript-eslint/no-shadow
- O.map(({ failure, ...rest }) =>
- Errors.extractErrorMessageFromIssuerConf(failure.reason.errorCode, rest)
+ O.map(({ errorCode, ...rest }) =>
+ Errors.extractErrorMessageFromIssuerConf(errorCode, rest)
),
O.map(message => message?.[getClaimsFullLocale()]),
O.toUndefined
);
+
+ return {
+ message: localizedMessage,
+ errorCode: pipe(errorCodeOption, O.toUndefined)
+ };
};
diff --git a/ts/features/itwallet/machine/credential/failure.ts b/ts/features/itwallet/machine/credential/failure.ts
index 6e5cf094dac..2c9741b5303 100644
--- a/ts/features/itwallet/machine/credential/failure.ts
+++ b/ts/features/itwallet/machine/credential/failure.ts
@@ -1,48 +1,43 @@
-import { enumType } from "@pagopa/ts-commons/lib/types";
-import * as t from "io-ts";
import { Errors } from "@pagopa/io-react-native-wallet";
-import { assert } from "../../../../utils/assert";
import { CredentialIssuanceEvents } from "./events";
-export enum CredentialIssuanceFailureTypeEnum {
- GENERIC = "GENERIC",
+const {
+ isIssuerResponseError,
+ isWalletProviderResponseError,
+ IssuerResponseErrorCodes: Codes
+} = Errors;
+
+export enum CredentialIssuanceFailureType {
+ UNEXPECTED = "UNEXPECTED",
ASYNC_ISSUANCE = "ASYNC_ISSUANCE",
- INVALID_STATUS = "INVALID_STATUS"
+ INVALID_STATUS = "INVALID_STATUS",
+ ISSUER_GENERIC = "ISSUER_GENERIC",
+ WALLET_PROVIDER_GENERIC = "WALLET_PROVIDER_GENERIC"
}
-export const CredentialIssuanceFailureType =
- enumType(
- CredentialIssuanceFailureTypeEnum,
- "CredentialIssuanceFailureTypeEnum"
- );
-
-export type CredentialIssuanceFailureType = t.TypeOf<
- typeof CredentialIssuanceFailureType
->;
-
-const CredentialIssuanceFailureR = t.type({
- type: CredentialIssuanceFailureType
-});
-
-const CredentialIssuanceFailureO = t.partial({
- reason: t.unknown
-});
-
-export const CredentialIssuanceFailure = t.intersection(
- [CredentialIssuanceFailureR, CredentialIssuanceFailureO],
- "CredentialIssuanceFailure"
-);
+/**
+ * Type that maps known reasons with the corresponding failure, in order to avoid unknowns as much as possible.
+ */
+export type ReasonTypeByFailure = {
+ [CredentialIssuanceFailureType.ISSUER_GENERIC]: Errors.IssuerResponseError;
+ [CredentialIssuanceFailureType.INVALID_STATUS]: Errors.IssuerResponseError;
+ [CredentialIssuanceFailureType.ASYNC_ISSUANCE]: Errors.IssuerResponseError;
+ [CredentialIssuanceFailureType.WALLET_PROVIDER_GENERIC]: Errors.WalletProviderResponseError;
+ [CredentialIssuanceFailureType.UNEXPECTED]: unknown;
+};
-export type CredentialIssuanceFailure = t.TypeOf<
- typeof CredentialIssuanceFailure
->;
+type TypedCredentialIssuanceFailures = {
+ [K in CredentialIssuanceFailureType]: {
+ type: K;
+ reason: ReasonTypeByFailure[K];
+ };
+};
-export const isCredentialInvalidStatusError = (
- error: CredentialIssuanceFailure
-): error is {
- type: CredentialIssuanceFailureTypeEnum.INVALID_STATUS;
- reason: Errors.CredentialInvalidStatusError;
-} => error.type === CredentialIssuanceFailureTypeEnum.INVALID_STATUS;
+/*
+ * Union type of failures with the reason properly typed.
+ */
+export type CredentialIssuanceFailure =
+ TypedCredentialIssuanceFailures[keyof TypedCredentialIssuanceFailures];
/**
* Maps an event dispatched by the credential issuance machine to a failure object.
@@ -54,32 +49,43 @@ export const isCredentialInvalidStatusError = (
export const mapEventToFailure = (
event: CredentialIssuanceEvents
): CredentialIssuanceFailure => {
- try {
- assert("error" in event && event.error, "Not an error event");
- const error = event.error;
+ // This should never happen
+ if (!("error" in event)) {
+ return { type: CredentialIssuanceFailureType.UNEXPECTED, reason: null };
+ }
+
+ const { error } = event;
- if (error instanceof Errors.CredentialInvalidStatusError) {
- return {
- type: CredentialIssuanceFailureTypeEnum.INVALID_STATUS,
- reason: error
- };
- }
+ if (isIssuerResponseError(error, Codes.CredentialInvalidStatus)) {
+ return {
+ type: CredentialIssuanceFailureType.INVALID_STATUS,
+ reason: error
+ };
+ }
- if (error instanceof Errors.CredentialIssuingNotSynchronousError) {
- return {
- type: CredentialIssuanceFailureTypeEnum.ASYNC_ISSUANCE,
- reason: error
- };
- }
+ if (isIssuerResponseError(error, Codes.CredentialIssuingNotSynchronous)) {
+ return {
+ type: CredentialIssuanceFailureType.ASYNC_ISSUANCE,
+ reason: error
+ };
+ }
+ if (isIssuerResponseError(error)) {
return {
- type: CredentialIssuanceFailureTypeEnum.GENERIC,
+ type: CredentialIssuanceFailureType.ISSUER_GENERIC,
reason: error
};
- } catch (e) {
+ }
+
+ if (isWalletProviderResponseError(error)) {
return {
- type: CredentialIssuanceFailureTypeEnum.GENERIC,
- reason: e
+ type: CredentialIssuanceFailureType.WALLET_PROVIDER_GENERIC,
+ reason: error
};
}
+
+ return {
+ type: CredentialIssuanceFailureType.UNEXPECTED,
+ reason: error
+ };
};