Skip to content

Commit

Permalink
feat(IT Wallet): [SIW-1036] Add eID detail screen (#5801)
Browse files Browse the repository at this point in the history
> [!WARNING]  
> This PR depends on #5780

## Short description

This PR adds the credential detail screen for the eID. The screen can
support other credential types as well, it would just need a bit more
fine tuning.

> [!Note]
> In the detail screen claims are grouped into sections, but this
information is not included in the credential itself. To work around
this, a mapping claim-section has been hardcoded in app. Claims that can
not be assigned to any section are displayed after all the others.
> The **eID credential** used is also a **mock**.

## List of changes proposed in this pull request
- Extracted `RenderPidAssuranceLevel` and `RenderReleaserName` into
separate components for easier reuse
- Added `ItwPresentationEidDetailScreen` component with related
sub-components
- Modified `ItwCredentialClaim` to support hidden claims and dates with
icon and badge
- Add utils in `itwClaimsUtils` to handle hardcoded claims
configurations

## How to test
Navigate to **Profile > IT Wallet > Credential detail (eID)**. You
should be able to see the eID detail screen.

## Preview

|iOS|Android|
|---|--------|
|<video
src="https://github.com/pagopa/io-app/assets/52289599/03ab3b16-838f-4eb0-bbe4-006f1324692e">|<video
src="https://github.com/pagopa/io-app/assets/52289599/7a7a7feb-f994-4669-9083-b398311d0086">|

---------

Co-authored-by: Federico Mastrini <[email protected]>
Co-authored-by: Damiano Plebani <[email protected]>
Co-authored-by: Mario Perrotta <[email protected]>
  • Loading branch information
4 people authored Jun 17, 2024
1 parent c0fe107 commit f6e0e19
Show file tree
Hide file tree
Showing 21 changed files with 1,742 additions and 123 deletions.
14 changes: 14 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3222,6 +3222,20 @@ features:
unsupportedDevice:
text: Starting from 02.07.2024 your device may no longer be compatible with IO.
moreInfo: More info
presentation:
credentialDetails:
personalDataTitle: "Personal data"
documentDataTitle: "Document data"
lastUpdated: "Last updated on {{lastUpdateTime}}"
hiddenClaim: "Hidden"
status:
valid: Valid
expired: Expired
actions:
removeFromWallet: "Remove from Wallet"
requestAssistance: "Something wrong? Contact {{authSource}}"
showClaimValues: "Show claim values"
hideClaimValues: "Hide claim values"
webView:
error:
missingParams: Not all information necessary to access this page are available
Expand Down
14 changes: 14 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3222,6 +3222,20 @@ features:
unsupportedDevice:
text: A partire dal 02.07.2024 il tuo dispositivo potrebbe non essere più compatibile con IO.
moreInfo: Scopri perchè
presentation:
credentialDetails:
personalDataTitle: "Dati personali"
documentDataTitle: "Dati documento"
lastUpdated: "Dati aggiornati al {{lastUpdateTime}}"
hiddenClaim: "Nascosto"
status:
valid: Valida
expired: Scaduta
actions:
removeFromWallet: "Rimuovi dal Portafoglio"
requestAssistance: "Qualcosa non torna? Contatta {{authSource}}"
showClaimValues: "Mostra gli attributi del documento"
hideClaimValues: "Nascondi gli attributi del documento"
webView:
error:
missingParams: Non sono presenti le informazioni necessarie per accedere a questa pagina
Expand Down
75 changes: 67 additions & 8 deletions ts/features/itwallet/common/components/ItwCredentialClaim.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import { Divider, ListItemInfo } from "@pagopa/io-app-design-system";
import * as E from "fp-ts/Either";
import { pipe } from "fp-ts/lib/function";
Expand All @@ -7,18 +7,24 @@ import { DateFromString } from "@pagopa/ts-commons/lib/dates";
import {
ClaimDisplayFormat,
ClaimValue,
DateClaimConfig,
DrivingPrivilegesClaim,
DrivingPrivilegesClaimType,
EvidenceClaim,
ImageClaim,
PlaceOfBirthClaim,
PlaceOfBirthClaimType,
PlainTextClaim
PlainTextClaim,
dateClaimsConfig,
previewDateClaimsConfig
} from "../utils/itwClaimsUtils";
import I18n from "../../../../i18n";
import { useItwInfoBottomSheet } from "../hooks/useItwInfoBottomSheet";
import { localeDateFormat } from "../../../../utils/locale";
import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet";
import { getExpireStatus } from "../../../../utils/dates";

const HIDDEN_CLAIM = "******";

/**
* Component which renders a place of birth type claim.
Expand Down Expand Up @@ -57,28 +63,63 @@ const PlainTextClaimItem = ({
<ListItemInfo
label={label}
value={claim}
accessibilityLabel={`${label} ${claim}`}
accessibilityLabel={`${label} ${
claim === HIDDEN_CLAIM
? I18n.t(
"features.itWallet.presentation.credentialDetails.hiddenClaim"
)
: claim
}`}
/>
<Divider />
</>
);

/**
* Component which renders a date type claim.
* Component which renders a date type claim with an optional icon and expiration badge.
* @param label - the label of the claim
* @param claim - the value of the claim
*/
const DateClaimItem = ({ label, claim }: { label: string; claim: Date }) => {
const DateClaimItem = ({
label,
claim,
iconVisible,
expirationBadgeVisible
}: {
label: string;
claim: Date;
} & DateClaimConfig) => {
const value = localeDateFormat(
claim,
I18n.t("global.dateFormats.shortFormat")
);

const endElement: ListItemInfo["endElement"] = useMemo(() => {
if (!expirationBadgeVisible) {
return;
}
const isExpired = getExpireStatus(claim) === "EXPIRED";
return {
type: "badge",
componentProps: {
variant: isExpired ? "error" : "success",
text: I18n.t(
`features.itWallet.presentation.credentialDetails.status.${
isExpired ? "expired" : "valid"
}`
)
}
};
}, [expirationBadgeVisible, claim]);

return (
<View key={`${label}-${value}`}>
<ListItemInfo
label={label}
value={value}
icon={iconVisible ? "calendar" : undefined}
accessibilityLabel={`${label} ${value}`}
endElement={endElement}
/>
<Divider />
</View>
Expand Down Expand Up @@ -239,17 +280,35 @@ const DrivingPrivilegesClaimItem = ({
* It renders a different component based on the type of the claim.
* @param claim - the claim to render
*/
export const ItwCredentialClaim = ({ claim }: { claim: ClaimDisplayFormat }) =>
export const ItwCredentialClaim = ({
claim,
hidden,
isPreview
}: {
claim: ClaimDisplayFormat;
hidden?: boolean;
isPreview?: boolean;
}) =>
pipe(
claim.value,
ClaimValue.decode,
E.fold(
() => <UnknownClaimItem label={claim.label} />,
decoded => {
_decoded => {
const decoded = hidden ? HIDDEN_CLAIM : _decoded;
if (PlaceOfBirthClaim.is(decoded)) {
return <PlaceOfBirthClaimItem label={claim.label} claim={decoded} />;
} else if (DateFromString.is(decoded)) {
return <DateClaimItem label={claim.label} claim={decoded} />;
const dateClaimProps = isPreview
? previewDateClaimsConfig
: dateClaimsConfig[claim.id];
return (
<DateClaimItem
label={claim.label}
claim={decoded}
{...dateClaimProps}
/>
);
} else if (EvidenceClaim.is(decoded)) {
return (
<EvidenceClaimItem
Expand Down
121 changes: 11 additions & 110 deletions ts/features/itwallet/common/components/ItwCredentialClaimList.tsx
Original file line number Diff line number Diff line change
@@ -1,135 +1,36 @@
import { ListItemInfo } from "@pagopa/io-app-design-system";
import { SdJwt } from "@pagopa/io-react-native-wallet";
import React from "react";
import { Alert, View } from "react-native";
import I18n from "../../../../i18n";
import { useItwInfoBottomSheet } from "../hooks/useItwInfoBottomSheet";
import { View } from "react-native";
import { parseClaims, sortClaims } from "../utils/itwClaimsUtils";
import { CredentialType, mapAssuranceLevel } from "../utils/itwMocksUtils";
import { StoredCredential } from "../utils/itwTypesUtils";
import { ItwCredentialClaim } from "./ItwCredentialClaim";
import { ItwReleaserName } from "./ItwReleaserName";
import { ItwPidAssuranceLevel } from "./ItwPidAssuranceLevel";

/**
* 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.
*/
export const ItwCredentialClaimsList = ({
data: {
parsedCredential,
displayData,
issuerConf,
credential,
credentialType
}
data,
isPreview
}: {
data: StoredCredential;
isPreview?: boolean;
}) => {
const claims = parseClaims(sortClaims(displayData.order, parsedCredential));

/**
* Renders the releaser name with an info button that opens the bottom sheet.
* This is not part of the claims list because it's not a claim.
* Thus it's rendered separately.
* @param releaserName - the releaser name.
* @returns the list item with the releaser name.
*/
const RenderReleaserName = () => {
const releaserName = issuerConf.federation_entity.organization_name;
const label = I18n.t(
"features.itWallet.verifiableCredentials.claims.releasedBy"
);
const releasedByBottomSheet = useItwInfoBottomSheet({
title:
releaserName ??
I18n.t("features.itWallet.generic.placeholders.organizationName"),
content: [
{
title: I18n.t(
"features.itWallet.issuance.credentialPreview.bottomSheet.about.title"
),
body: I18n.t(
"features.itWallet.issuance.credentialPreview.bottomSheet.about.subtitle"
)
},
{
title: I18n.t(
"features.itWallet.issuance.credentialPreview.bottomSheet.data.title"
),
body: I18n.t(
"features.itWallet.issuance.credentialPreview.bottomSheet.data.subtitle"
)
}
]
});
const { parsedCredential, displayData } = data;

return (
<>
{releaserName ? (
<>
<ListItemInfo
endElement={{
type: "iconButton",
componentProps: {
icon: "info",
accessibilityLabel: "test",
onPress: () => releasedByBottomSheet.present()
}
}}
label={label}
value={releaserName}
accessibilityLabel={`${label} ${releaserName}`}
/>
{releasedByBottomSheet.bottomSheet}
</>
) : null}
</>
);
};

/**
* Renders the PID assurance level with an info button that currenlt navigates to a not available screen.
* If the credential is not a PID credential, it returns null.
* This is not part of the claims list because it's not a claim.
* Thus it's rendered separately.
* @returns the list item with the PID assurance level.
*/
const RenderPidAssuranceLevel = () => {
if (credentialType === CredentialType.PID) {
const { sdJwt } = SdJwt.decode(credential, SdJwt.SdJwt4VC);
const assuranceLevel = mapAssuranceLevel(
sdJwt.payload.verified_claims.verification.assurance_level
);
return (
<ListItemInfo
label={I18n.t(
"features.itWallet.verifiableCredentials.claims.securityLevel"
)}
value={assuranceLevel}
endElement={{
type: "iconButton",
componentProps: {
icon: "info",
onPress: () => Alert.alert("Not available"),
accessibilityLabel: ""
}
}}
/>
);
} else {
return null;
}
};
const claims = parseClaims(sortClaims(displayData.order, parsedCredential));

return (
<>
{claims.map((elem, index) => (
<View key={index}>
<ItwCredentialClaim claim={elem} />
<ItwCredentialClaim claim={elem} isPreview={isPreview} />
</View>
))}
<RenderReleaserName />
<RenderPidAssuranceLevel />
<ItwReleaserName credential={data} />
<ItwPidAssuranceLevel credential={data} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useState } from "react";
import { StyleSheet, View } from "react-native";
import { IconButton, H6 } from "@pagopa/io-app-design-system";
import I18n from "../../../../i18n";
import { ClaimDisplayFormat } from "../utils/itwClaimsUtils";
import { ItwCredentialClaim } from "./ItwCredentialClaim";

type Props = {
title: string;
claims: ReadonlyArray<ClaimDisplayFormat>;
canHideValues?: boolean;
};

export const ItwCredentialClaimsSection = ({
title,
canHideValues,
claims
}: Props) => {
const [valuesHidden, setValuesHidden] = useState(false);

const renderHideValuesToggle = () => (
<IconButton
testID="toggle-claim-visibility"
icon={valuesHidden ? "eyeHide" : "eyeShow"}
onPress={() => setValuesHidden(x => !x)}
accessibilityLabel={I18n.t(
valuesHidden
? "features.itWallet.presentation.credentialDetails.actions.showClaimValues"
: "features.itWallet.presentation.credentialDetails.actions.hideClaimValues"
)}
/>
);

return (
<View>
<View style={styles.header}>
<H6 color="grey-700">{title}</H6>
{canHideValues && renderHideValuesToggle()}
</View>
<View>
{claims.map(c => (
<ItwCredentialClaim key={c.id} claim={c} hidden={valuesHidden} />
))}
</View>
</View>
);
};

const styles = StyleSheet.create({
header: {
justifyContent: "space-between",
flexDirection: "row"
}
});
Loading

0 comments on commit f6e0e19

Please sign in to comment.