Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: proof request missing attribute #1307

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions packages/legacy/core/App/components/misc/CredentialCard11.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ const CredentialCard11: React.FC<CredentialCard11Props> = ({
const secondaryField = overlay?.presentationFields?.find(
(field) => field.name === overlay?.brandingOverlay?.secondaryAttribute
)

return [...(displayItems ?? []), primaryField, secondaryField]
}, [displayItems, overlay])

Expand Down Expand Up @@ -382,7 +381,6 @@ const CredentialCard11: React.FC<CredentialCard11Props> = ({

const AttributeLabel: React.FC<{ label: string }> = ({ label }) => {
const ylabel = overlay.bundle?.labelOverlay?.attributeLabels[label] ?? startCase(label)

return (
<Text
style={[
Expand All @@ -400,6 +398,23 @@ const CredentialCard11: React.FC<CredentialCard11Props> = ({
)
}

const AttributeErrorLabel: React.FC<{ errorMessage: string }> = ({ errorMessage }) => {
return (
<Text
style={[
TextTheme.labelSubtitle,
styles.textContainer,
{
lineHeight: 19,
opacity: 0.8,
},
]}
>
{errorMessage}
</Text>
)
}

const AttributeValue: React.FC<{ value: string | number | null; warn?: boolean }> = ({ value, warn }) => {
return (
<>
Expand Down Expand Up @@ -427,18 +442,32 @@ const CredentialCard11: React.FC<CredentialCard11Props> = ({
const renderCardAttribute = (item: Attribute & Predicate) => {
const { label, value } = parseAttribute(item)
const parsedValue = formatIfDate(item?.format, value) ?? ''

return (
item && (
<View style={{ marginTop: 15 }}>
{!(item?.value || item?.satisfied) ? (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Icon
style={{ paddingTop: 2, paddingHorizontal: 2 }}
name="close"
color={ListItems.proofError.color}
size={ListItems.recordAttributeText.fontSize}
/>
<AttributeLabel label={label} />
<View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Icon
style={{ paddingTop: 2, paddingHorizontal: 2 }}
name="close"
color={ListItems.proofError.color}
size={ListItems.recordAttributeText.fontSize}
/>
<AttributeLabel label={label} />
</View>
{item.errorMessage && (
<View style={{ flexDirection: 'row', alignItems: 'center', marginStart: 15 }}>
<Icon
style={{ paddingTop: 2, paddingHorizontal: 2 }}
name="close"
color={ListItems.proofError.color}
size={ListItems.recordAttributeText.fontSize}
/>
<AttributeErrorLabel errorMessage={item.errorMessage} />
</View>
)}
</View>
) : (
<AttributeLabel label={label} />
Expand Down
1 change: 1 addition & 0 deletions packages/legacy/core/App/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ const translation = {
"CredentialMetadataNotFound": "Could not find credential metadata",
"NewProofRequest": "New Proof Request",
"NotAvailableInYourWallet": "Not in your wallet",
"MissingAttribute": "{{ name }} is missing",
"PredicateNotSatisfied": "Requirement not met",
"IsRequesting": "is requesting",
"IsRequestingSomethingYouDontHaveAvailable": "is requesting something you don't have available",
Expand Down
1 change: 1 addition & 0 deletions packages/legacy/core/App/localization/fr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ const translation = {
"CredentialMetadataNotFound": "Impossible de trouver les métadonnées des attestations",
"NewProofRequest": "Nouvelle demande de preuve",
"NotAvailableInYourWallet": "Non disponible dans votre portefeuille",
"MissingAttribute": "{{ name }} is missing (FR)",
"PredicateNotSatisfied": "Requête non satisfaite",
"IsRequesting": "demande",
"IsRequestingSomethingYouDontHaveAvailable": "demande quelque chose que vous n'avez pas à votre disposition",
Expand Down
1 change: 1 addition & 0 deletions packages/legacy/core/App/localization/pt-br/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ const translation = {
"CredentialMetadataNotFound": "Não foi possível encontrar metadados de credenciais",
"NewProofRequest": "Nova Prova de Credencial",
"NotAvailableInYourWallet": "Não disponível em sua carteira",
"MissingAttribute": "{{ name }} is missing (PT-BR)",
"PredicateNotSatisfied": "Requisito não atendido",
"IsRequesting": "está requisitando",
"IsRequestingSomethingYouDontHaveAvailable": "está requisitando algo que você não tem disponível",
Expand Down
32 changes: 4 additions & 28 deletions packages/legacy/core/App/screens/ProofRequest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ const ProofRequest: React.FC<ProofRequestProps> = ({ navigation, proofId }) => {
fields: {
[key: string]: Attribute[] & Predicate[]
}
) => {
): boolean => {
const revList = credExRecords.map((cred) => {
return {
id: cred.credentials.map((item) => item.credentialRecordId),
Expand Down Expand Up @@ -238,6 +238,7 @@ const ProofRequest: React.FC<ProofRequestProps> = ({ navigation, proofId }) => {
setLoading(false)
setDescriptorMetadata(descriptorMetadata)

// Credentials that satisfy the proof request
let credList: string[] = []
if (selectedCredentials.length > 0) {
credList = selectedCredentials
Expand Down Expand Up @@ -282,6 +283,7 @@ const ProofRequest: React.FC<ProofRequestProps> = ({ navigation, proofId }) => {
>,
}
: undefined

setRetrievedCredentials(selectRetrievedCredentials)

const activeCreds = groupedProof.filter((item: any) => credList.includes(item.credId))
Expand All @@ -294,7 +296,7 @@ const ProofRequest: React.FC<ProofRequestProps> = ({ navigation, proofId }) => {
return { ...prev, [current.credId]: current.attributes ?? current.predicates ?? [] }
}, {})
}

// Check for revoked credentials
const records = fullCredentials.filter((record: any) =>
record.credentials.some((cred: any) => credList.includes(cred.credentialRecordId))
)
Expand Down Expand Up @@ -692,32 +694,6 @@ const ProofRequest: React.FC<ProofRequestProps> = ({ navigation, proofId }) => {
/>
{!hasAvailableCredentials && (
<CredentialList
header={
<View style={styles.pageMargin}>
{!(loading || attestationLoading) && (
<>
{hasMatchingCredDef && (
<View
style={{
width: 'auto',
borderWidth: 1,
borderColor: ColorPallet.grayscale.lightGrey,
marginTop: 20,
}}
/>
)}
<Text
style={{
...TextTheme.title,
marginTop: 10,
}}
>
{t('ProofRequest.MissingCredentials')}
</Text>
</>
)}
</View>
}
footer={proofPageFooter()}
items={activeCreds.filter((cred) => cred.credExchangeRecord === undefined) ?? []}
/>
Expand Down
58 changes: 54 additions & 4 deletions packages/legacy/core/App/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ProofState,
parseDid,
OutOfBandRecord,
CredentialPreviewAttribute,
} from '@credo-ts/core'
import { BasicMessageRole } from '@credo-ts/core/build/modules/basic-messages/BasicMessageRole'
import { useConnectionById } from '@credo-ts/react-hooks'
Expand Down Expand Up @@ -432,7 +433,31 @@ export const evaluatePredicates =
})
}

const addMissingDisplayAttributes = (attrReq: AnonCredsRequestedAttribute) => {
// Scans through given credential records and returns true if a given attribute is missing from the records
const isOffendingAttribute = (
attributeName: string,
idToFind: string,
credentialRecords: CredentialExchangeRecord[]
): boolean => {
// filter records for a given schema or credential definition id
const records = credentialRecords.filter(
(record: CredentialExchangeRecord) =>
getCredentialSchemaIdForRecord(record) === idToFind || getCredentialDefinitionIdForRecord(record) === idToFind
)

// then for each record, find if a given attribute exists
// if no attributes are found in the record, then the given attributeName is the offending attribute
return records.some(
(record: CredentialExchangeRecord) =>
!record.credentialAttributes?.some((attribute: CredentialPreviewAttribute) => attribute.name === attributeName)
)
}

const addMissingDisplayAttributes = (
attrReq: AnonCredsRequestedAttribute,
records: CredentialExchangeRecord[],
t: TFunction<'translation', undefined>
): ProofCredentialAttributes => {
const { name, names, restrictions } = attrReq
const credName = credNameFromRestriction(restrictions)
const credDefId = credDefIdFromRestrictions(restrictions)
Expand All @@ -456,12 +481,17 @@ const addMissingDisplayAttributes = (attrReq: AnonCredsRequestedAttribute) => {
credentialId: credName,
name: attributeName,
value: '',
// This assumes that schema id OR cred definition ids are present in the proof request
errorMessage: isOffendingAttribute(attributeName, schemaId || credDefId, records)
? t('ProofRequest.MissingAttribute', { name: attributeName })
: undefined,
})
)
}
return processedAttributes
}
export const processProofAttributes = (
t: TFunction<'translation', undefined>,
request?: AnonCredsProofRequest,
credentials?: AnonCredsCredentialsForProofRequest,
credentialRecords?: CredentialExchangeRecord[],
Expand All @@ -473,23 +503,29 @@ export const processProofAttributes = (
const retrievedCredentialAttributes = credentials?.attributes
const requestNonRevoked = request?.non_revoked // non_revoked interval can sometimes be top level

// no proof or credential attributes, nothing to process, leave early
if (!requestedProofAttributes || !retrievedCredentialAttributes) {
return {}
}

for (const key of Object.keys(retrievedCredentialAttributes)) {
const altCredentials = [...(retrievedCredentialAttributes[key] ?? [])]
.sort(credentialSortFn)
.map((cred) => cred.credentialId)

const credentialList = [...(retrievedCredentialAttributes[key] ?? [])].sort(credentialSortFn)

const { name, names, non_revoked, restrictions } = requestedProofAttributes[key]

// system assumes one of these values is present in a proof request
const proofCredDefId = credDefIdFromRestrictions(restrictions)
const proofSchemaId = schemaIdFromRestrictions(restrictions)

// No credentials satisfy proof request, process attribute errors

if (credentialList.length <= 0) {
const missingAttributes = addMissingDisplayAttributes(requestedProofAttributes[key])
console.log(JSON.stringify(requestedProofAttributes[key]))
const missingAttributes = addMissingDisplayAttributes(requestedProofAttributes[key], credentialRecords ?? [], t)

const missingCredGroupKey = groupByReferent ? key : missingAttributes.credName
if (!processedAttributes[missingCredGroupKey]) {
processedAttributes[missingCredGroupKey] = missingAttributes
Expand Down Expand Up @@ -547,6 +583,7 @@ export const processProofAttributes = (
}
}
}

return processedAttributes
}

Expand Down Expand Up @@ -827,6 +864,7 @@ export const retrieveCredentialsForProof = async (

const filtered = filterInvalidProofRequestMatches(anonCredsCredentialsForRequest, descriptorMetadata)
const processedAttributes = processProofAttributes(
t,
anonCredsProofRequest,
filtered,
fullCredentials,
Expand All @@ -851,7 +889,7 @@ export const retrieveCredentialsForProof = async (
const proofRequest = format.request?.anoncreds ?? format.request?.indy
const proofFormat = credentials.proofFormats.anoncreds ?? credentials.proofFormats.indy

const attributes = processProofAttributes(proofRequest, proofFormat, fullCredentials, groupByReferent)
const attributes = processProofAttributes(t, proofRequest, proofFormat, fullCredentials, groupByReferent)
const predicates = processProofPredicates(proofRequest, proofFormat, fullCredentials, groupByReferent)
const groupedProof = Object.values(mergeAttributesAndPredicates(attributes, predicates))

Expand Down Expand Up @@ -1098,6 +1136,18 @@ export function isChildFunction<T>(children: ReactNode | ChildFn<T>): children i
return typeof children === 'function'
}

// Fetches the credential definition id for a given record, returns null if ID is not set
const getCredentialDefinitionIdForRecord = (record: CredentialExchangeRecord): string | null => {
// assumes record is anonCred
return record.metadata.get('_anoncreds/credential')?.credentialDefinitionId ?? null
}

// Fetches the schema id for a given record, returns null if ID is not set
const getCredentialSchemaIdForRecord = (record: CredentialExchangeRecord): string | null => {
// assumes record is anonCred
return record.metadata.get('_anoncreds/credential')?.schemaId ?? null
}

export function getCredentialEventRole(record: CredentialExchangeRecord) {
switch (record.state) {
// assuming only Holder states are supported here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ exports[`Connection Screen Valid goal code aries.vc.issue extracted, show to off
Attribute {
"credentialId": undefined,
"encoding": undefined,
"errorMessage": undefined,
"format": undefined,
"label": undefined,
"mimeType": "text/plain",
Expand All @@ -822,6 +823,7 @@ exports[`Connection Screen Valid goal code aries.vc.issue extracted, show to off
Attribute {
"credentialId": undefined,
"encoding": undefined,
"errorMessage": undefined,
"format": undefined,
"label": undefined,
"mimeType": "text/plain",
Expand All @@ -836,6 +838,7 @@ exports[`Connection Screen Valid goal code aries.vc.issue extracted, show to off
Attribute {
"credentialId": undefined,
"encoding": undefined,
"errorMessage": undefined,
"format": undefined,
"label": undefined,
"mimeType": "text/plain",
Expand Down
Loading
Loading