Skip to content

Commit

Permalink
chore(IT Wallet): [SIW-722] Add verifyAndParseCredential phase to cre…
Browse files Browse the repository at this point in the history
…dential issuing (#5310)

## Short description
Depends on
[this](pagopa/io-react-native-wallet#71) PR.

This PR introduces the `verifyAndParseCredential` phase to the
credential issuing flow.

## List of changes proposed in this pull request
- Replaces the manual verify and parse phase in `ItwIssuanceSaga.ts`
with the library `verifyAndParseCredential` function which already
returns a Record of claims;
- Adjusts `ItwCredentialClaimsList.tsx` to get the claims from the new
Record structure. There's no need to use the
`credentialConfigurationSchema` anymore.

## How to test
Reset the wallet. Test a credential issuing flow, the claims should be
displayed just as before.

---------

Co-authored-by: Mario Perrotta <mario.perrotta@pagopa.it>
  • Loading branch information
LazyAfternoons and hevelius authored Dec 18, 2023
1 parent 727e5c9 commit 84e55db
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 96 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
"@pagopa/io-react-native-crypto": "^0.2.1",
"@pagopa/io-react-native-jwt": "1.1.0",
"@pagopa/io-react-native-login-utils": "^0.2.0",
"@pagopa/io-react-native-wallet": "^0.10.2",
"@pagopa/io-react-native-wallet": "^0.11.1",
"@pagopa/react-native-cie": "1.2.0",
"@pagopa/react-native-nodelibs": "^0.1.0",
"@pagopa/ts-commons": "^10.15.0",
Expand Down
7 changes: 4 additions & 3 deletions ts/features/it-wallet/components/ItwCredentialCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CredentialCatalogDisplay,
getImageFromCredentialType
} from "../utils/mocks";
import { ParsedCredential } from "../utils/types";

/**
* Common props for the component.
Expand All @@ -31,7 +32,7 @@ type WithPidProps = CommonProps & {
* @param parsedCredential - the parsed credential.
*/
type WithCredentialProps = CommonProps & {
parsedCredential: Record<string, string>;
parsedCredential: ParsedCredential;
};

type CredentialCardProps = WithPidProps | WithCredentialProps;
Expand Down Expand Up @@ -82,11 +83,11 @@ const ItwCredentialCard = (props: CredentialCardProps) => {
const { firstLine, secondLine } = props.display;
const flText =
firstLine && firstLine.length > 0
? firstLine.map(item => props.parsedCredential[item]).join(" ")
? firstLine.map(item => props.parsedCredential[item].value).join(" ")
: "";
const slText =
secondLine && secondLine.length > 0
? secondLine.map(item => props.parsedCredential[item]).join(" ")
? secondLine.map(item => props.parsedCredential[item].value).join(" ")
: "";
return { firstLine: flText, secondLine: slText };
}
Expand Down
128 changes: 58 additions & 70 deletions ts/features/it-wallet/components/ItwCredentialClaimsList.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import React from "react";
import { Divider, ListItemInfo } from "@pagopa/io-app-design-system";
import { View } from "react-native";
import * as O from "fp-ts/Option";
import * as t from "io-ts";
import { pipe } from "fp-ts/lib/function";
import * as E from "fp-ts/Either";
import { getClaimsFullLocale, localeDateFormatOrSame } from "../utils/locales";
import { getClaimsFullLocale } from "../utils/locales";
import I18n from "../../../i18n";
import { CredentialCatalogDisplay } from "../utils/mocks";
import { StoredCredential } from "../store/reducers/itwCredentialsReducer";
import { useItwInfoBottomSheet } from "../hooks/useItwInfoBottomSheet";
import { ParsedCredential } from "../utils/types";

/**
* Type of the claims list.
* Consists of a list of claims, each claim is a couple of label and value
* wrapped in an Option.
* Consists of a list of claims, each claim is a couple of label and value.
*/
type ClaimList = ReadonlyArray<{
value: O.Option<string>;
label: O.Option<string>;
type ClaimList = Array<{
label: string;
value: unknown;
}>;

/**
* Decoder type for the evidence field of the credential.
*/
const EvidenceDecoder = t.array(
const EvidenceClaimDecoder = t.array(
t.type({
type: t.string,
record: t.type({
Expand All @@ -38,77 +37,66 @@ const EvidenceDecoder = t.array(
})
);

/**
* Decoder for string claims.
*/
const StringClaimDecoder = t.string;

/**
* Parses the claims from the credential.
* It uses the credentialConfigurationSchema to get the label for each claim by
* creating an object with its entries and then mapping them to a list of claims.
* The key of the object is used to get the value from the parsedCredential.
* If the value is not available, the value is set to undefined which is then
* wrapped in an Option.
* If the value is a date, it is formatted using the localeDateFormat function which otherwise returns the same value.
* The value of the object is used to get the label from the credentialConfigurationSchema
* by filtering the display array for the current locale.
* If the label is not available for the current locale, the label is set to undefined which is then
* wrapped in an Option.
* The resulting list of claims is then flattened by two levels to get a list of claims.
* For each Record entry it maps the key and the attribute value to a label and a value.
* The label is taken from the attribute name which is either a string or a record of locale and string.
* If the type of the attribute name is string then when take it's value because locales have not been set.
* If the type of the attribute name is record then we take the value of the locale that matches the current locale.
* If there's no locale that matches the current locale then we take the attribute key as the name.
* The value is taken from the attribute value.
* @param parsedCredential - the parsed credential.
* @param schema - the issuance credentialConfigurationSchema of parsedCredential.
* @returns the list of claims of the credential contained in its configuration schema.
* @returns the {@link ClaimList} of the credential contained in its configuration schema.
*/
const parseClaims = (
parsedCredential: StoredCredential["parsedCredential"],
schema: StoredCredential["credentialConfigurationSchema"]
): ClaimList =>
Object.entries(schema)
.map(([key, elem]) => ({
value: O.fromNullable(localeDateFormatOrSame(parsedCredential[key])),
label: O.fromNullable(
elem.display.filter(e => e.locale === getClaimsFullLocale())[0]?.name
)
}))
.flat(2);
const parseClaims = (parsedCredential: ParsedCredential): ClaimList =>
Object.entries(parsedCredential).map(([key, attribute]) => {
const attributeName =
typeof attribute.name === "string"
? attribute.name
: attribute.name[getClaimsFullLocale()] || key;

return { label: attributeName, value: attribute.value };
});

/**
* Sorts the schema according to the order of the displayData.
* Sorts the parsedCredential according to the order of the displayData.
* If the order is not available, the schema is returned as is.
* @param schema - the issuance credentialConfigurationSchema of parsedCredential.
* @param parsedCredential - the parsed credential.
* @param order - the order of the displayData.
* @returns schema sorted according to the order of the displayData.
* @returns a new parsedCredential sorted according to the order of the displayData.
*/
const sortSchema = (
schema: StoredCredential["credentialConfigurationSchema"],
order: CredentialCatalogDisplay["order"]
const sortClaims = (
order: CredentialCatalogDisplay["order"],
parsedCredential: ParsedCredential
) =>
order
? Object.fromEntries(
Object.entries(schema)
Object.entries(parsedCredential)
.slice()
.sort(([key1], [key2]) => order.indexOf(key1) - order.indexOf(key2))
)
: schema;
: parsedCredential;

/**
* This component renders the list of claims for a credential.
* It dinamically renders the list of claims passed as claims prop in the order they are passed.
* @param data - the {@link StoredCredential} of the credential.
*/
const ItwCredentialClaimsList = ({
data: {
parsedCredential,
credentialConfigurationSchema,
displayData,
issuerConf
}
data: { parsedCredential, displayData, issuerConf }
}: {
data: StoredCredential;
}) => {
const claims = parseClaims(
parsedCredential,
sortSchema(credentialConfigurationSchema, displayData.order)
);
const claims = parseClaims(sortClaims(displayData.order, parsedCredential));

const evidence = EvidenceDecoder.decode(
JSON.parse(parsedCredential.evidence)
const evidence = EvidenceClaimDecoder.decode(
claims.find(claim => claim.label === "evidence")?.value
);
const releaserName = issuerConf.federation_entity.organization_name;

Expand Down Expand Up @@ -228,24 +216,24 @@ const ItwCredentialClaimsList = ({
return (
<>
{claims.map(
({ label, value }, index, _, key = `${index}_${label}` /* 🥷 */) => (
<View key={key}>
<ListItemInfo
label={O.getOrElse(() =>
I18n.t(
"features.itWallet.generic.placeholders.claimLabelNotAvailable"
)
)(label)}
value={O.getOrElse(() =>
I18n.t(
"features.itWallet.generic.placeholders.claimNotAvailable"
)
)(value)}
accessibilityLabel={`${label} ${value}`}
/>
<Divider />
</View>
)
({ label, value }, index, _, key = `${index}_${label}` /* 🥷 */) =>
pipe(
value,
StringClaimDecoder.decode,
E.fold(
() => null,
() => (
<View key={key}>
<ListItemInfo
label={label}
value={value}
accessibilityLabel={`${label} ${value}`}
/>
<Divider />
</View>
)
)
)
)}
{E.isRight(evidence) && (
<>
Expand Down
24 changes: 11 additions & 13 deletions ts/features/it-wallet/saga/new/itwIssuanceSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {
Credential,
createCryptoContextFor
} from "@pagopa/io-react-native-wallet";
import * as SDJWT from "@pagopa/io-react-native-wallet/src/sd-jwt";
import { SdJwt4VC } from "@pagopa/io-react-native-wallet/src/sd-jwt/types";
import { CryptoContext } from "@pagopa/io-react-native-jwt";
import { ActionType, isActionOf } from "typesafe-actions";
import { toError } from "fp-ts/lib/Either";
Expand Down Expand Up @@ -169,27 +167,27 @@ export function* handleIssuanceGetCredential(): SagaIterator {
{ walletInstanceAttestation, walletProviderBaseUrl }
);

// obtain cred
// obtain credential
const { credential, format } = yield* call(
Credential.Issuance.obtainCredential,
issuerConf,
accessToken,
nonce,
clientId,
credentialType,
{ credentialCryptoContext, walletProviderBaseUrl }
"vc+sd-jwt",
{
walletProviderBaseUrl,
credentialCryptoContext
}
);

// TODO(SIW-659): replace with the VerificationAndParseCredential function
const parsedCredential = SDJWT.decode(
const { parsedCredential } = yield* call(
Credential.Issuance.verifyAndParseCredential,
issuerConf,
credential,
SdJwt4VC
).disclosures.reduce(
(p, { decoded: [, key, value] }) => ({
...p,
[key]: typeof value === "string" ? value : JSON.stringify(value)
}),
{} as Record<string, string>
format,
{ credentialCryptoContext, ignoreMissingAttributes: true }
);

yield* put(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CredentialConfigurationSchema,
CredentialDefinition,
IssuerConfiguration,
ParsedCredential,
PidResponse
} from "../../utils/types";
import { itwLifecycleOperational } from "../actions/itwLifecycleActions";
Expand All @@ -24,7 +25,7 @@ export type StoredCredential = {
keyTag: string;
credential: string;
format: string;
parsedCredential: Record<string, string>;
parsedCredential: ParsedCredential;
credentialConfigurationSchema: CredentialConfigurationSchema;
credentialType: string;
issuerConf: IssuerConfiguration;
Expand Down
17 changes: 13 additions & 4 deletions ts/features/it-wallet/utils/pid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,27 @@ export const getPid = async (
}
);

// Credential request
const { credential, format } = await Credential.Issuance.obtainCredential(
entityConfiguration,
accessToken,
nonce,
authConf.clientId,
PID_CREDENTIAL_TYPE,
"vc+sd-jwt",
{
credentialCryptoContext,
walletProviderBaseUrl
walletProviderBaseUrl,
credentialCryptoContext
}
);

return { credential, format, entityConfiguration };
// TODO: SIW-766 add verifyAndParseCredential for PID
// const { parsedCredential } =
// await Credential.Issuance.verifyAndParseCredential(
// entityConfiguration,
// credential,
// format,
// { credentialCryptoContext, ignoreMissingAttributes: true }
// );

return { credential, format, nonce, entityConfiguration };
};
7 changes: 7 additions & 0 deletions ts/features/it-wallet/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,10 @@ export type CredentialConfigurationSchema =
export type IssuerConfiguration = Awaited<
ReturnType<typeof Credential.Issuance.evaluateIssuerTrust>
>["issuerConf"];

/**
* Alias for the ParseCredential type
*/
export type ParsedCredential = Awaited<
ReturnType<typeof Credential.Issuance.verifyAndParseCredential>
>["parsedCredential"];
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3177,10 +3177,10 @@
resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-login-utils/-/io-react-native-login-utils-0.2.0.tgz#4a9b55d84c6d77622e95de4511e4c422a862b27d"
integrity sha512-mZ0Z7SAhWhbYIK/GvaeuecIyQPikj/D7pmejOrAiLA1KxIqO3UGH6p6LC2pZhN0JPHTcXbBiNHJRQtrqTHCfew==

"@pagopa/io-react-native-wallet@^0.10.2":
version "0.10.2"
resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-wallet/-/io-react-native-wallet-0.10.2.tgz#7b7a947f734da182fdb2db847a615500e4b9fcb5"
integrity sha512-nvL/HlcfRCKbDIg+Ca/LN7X4zpL93X5JHJaspqc2wgcxbgt7pCCeyFVfps65Tm4X3P/OAD1Z+jzXiFRh3IR0eg==
"@pagopa/io-react-native-wallet@^0.11.1":
version "0.11.1"
resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-wallet/-/io-react-native-wallet-0.11.1.tgz#0a23f1f9444982bfdc16e9d302978d8bdcaa3467"
integrity sha512-UURorbS0QWCyByCgibKiXi7zIhHwrsmP0PRCUk2PczGe2afbOiLbfoeV49/fDEz4Jz2l/ep1gDimrJ6MaxJGVw==
dependencies:
react-native-url-polyfill "^2.0.0"
react-native-uuid "^2.0.1"
Expand Down

0 comments on commit 84e55db

Please sign in to comment.