Skip to content

Commit

Permalink
refactor: credential issuance failures
Browse files Browse the repository at this point in the history
  • Loading branch information
gispada committed Nov 22, 2024
1 parent 1363a2d commit 921b90a
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 })
]);
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ 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";
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;

Expand Down
5 changes: 0 additions & 5 deletions ts/features/itwallet/credentials/saga/types.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,7 +24,6 @@ import { useAvoidHardwareBackButton } from "../../../../utils/useAvoidHardwareBa
import {
CREDENTIALS_MAP,
trackAddCredentialFailure,
trackAddCredentialTimeout,
trackCredentialInvalidStatusFailure,
trackCredentialNotEntitledFailure,
trackItWalletDeferredIssuing,
Expand All @@ -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 =
Expand All @@ -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" });
Expand All @@ -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 <OperationResultScreenContent {...resultScreenProps} />;
};

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(
Expand All @@ -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)
};
};
Loading

0 comments on commit 921b90a

Please sign in to comment.