diff --git a/packages/legacy/core/App/components/misc/CredentialCard.tsx b/packages/legacy/core/App/components/misc/CredentialCard.tsx index 54738cfd35..364b855ef2 100644 --- a/packages/legacy/core/App/components/misc/CredentialCard.tsx +++ b/packages/legacy/core/App/components/misc/CredentialCard.tsx @@ -21,6 +21,8 @@ interface CredentialCardProps { displayItems?: (Attribute | Predicate)[] existsInWallet?: boolean satisfiedPredicates?: boolean + hasAltCredentials?: boolean + handleAltCredChange?: () => void } const CredentialCard: React.FC = ({ @@ -32,6 +34,8 @@ const CredentialCard: React.FC = ({ credName, existsInWallet, satisfiedPredicates, + hasAltCredentials, + handleAltCredChange, style = {}, onPress = undefined, }) => { @@ -50,6 +54,8 @@ const CredentialCard: React.FC = ({ credDefId={credDefId} schemaId={schemaId} credential={credential} + handleAltCredChange={handleAltCredChange} + hasAltCredentials={hasAltCredentials} proof elevated > diff --git a/packages/legacy/core/App/components/misc/CredentialCard11.tsx b/packages/legacy/core/App/components/misc/CredentialCard11.tsx index a96493315e..e688fc2392 100644 --- a/packages/legacy/core/App/components/misc/CredentialCard11.tsx +++ b/packages/legacy/core/App/components/misc/CredentialCard11.tsx @@ -30,6 +30,8 @@ interface CredentialCard11Props { credDefId?: string schemaId?: string proof?: boolean + hasAltCredentials?: boolean + handleAltCredChange?: () => void } /* @@ -73,6 +75,8 @@ const CredentialCard11: React.FC = ({ credDefId, schemaId, proof, + hasAltCredentials, + handleAltCredChange, }) => { const { width } = useWindowDimensions() const borderRadius = 10 @@ -192,6 +196,22 @@ const CredentialCard11: React.FC = ({ fontSize: 22, transform: [{ rotate: '-30deg' }], }, + selectedCred: { + borderWidth: 5, + borderRadius: 15, + borderColor: ColorPallet.semantic.focus, + }, + seperator: { + width: '100%', + height: 2, + marginVertical: 10, + backgroundColor: ColorPallet.grayscale.lightGrey, + }, + credActionText: { + fontSize: 20, + fontWeight: 'bold', + color: ColorPallet.brand.link, + }, }) const parseAttribute = (item: (Attribute & Predicate) | undefined) => { @@ -422,6 +442,26 @@ const CredentialCard11: React.FC = ({ renderItem={({ item }) => { return renderCardAttribute(item as Attribute & Predicate) }} + ListFooterComponent={ + hasAltCredentials ? ( + + + + + {t('ProofRequest.ChangeCredential')} + + + + + ) : null + } /> ) @@ -499,7 +539,10 @@ const CredentialCard11: React.FC = ({ } return ( - + ) @@ -531,7 +574,12 @@ const CredentialCard11: React.FC = ({ } return overlay.bundle ? ( { setDimensions({ cardHeight: event.nativeEvent.layout.height, cardWidth: event.nativeEvent.layout.width }) }} diff --git a/packages/legacy/core/App/hooks/proofs.ts b/packages/legacy/core/App/hooks/proofs.ts index 9c4b7ae294..dedcac47ed 100644 --- a/packages/legacy/core/App/hooks/proofs.ts +++ b/packages/legacy/core/App/hooks/proofs.ts @@ -1,6 +1,9 @@ import { ProofExchangeRecord } from '@aries-framework/core' -import { useProofs } from '@aries-framework/react-hooks' +import { useAgent, useCredentials, useProofById, useProofs } from '@aries-framework/react-hooks' import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +import { retrieveCredentialsForProof } from '../utils/helpers' export const useProofsByConnectionId = (connectionId: string): ProofExchangeRecord[] => { const { records: proofs } = useProofs() @@ -9,3 +12,16 @@ export const useProofsByConnectionId = (connectionId: string): ProofExchangeReco [proofs, connectionId] ) } + +export const useAllCredentialsForProof = (proofId: string) => { + const { t } = useTranslation() + const { agent } = useAgent() + const fullCredentials = useCredentials().records + const proof = useProofById(proofId) + return useMemo(() => { + if (!proof || !agent) { + return + } + return retrieveCredentialsForProof(agent, proof, fullCredentials, t) + }, [proofId]) +} diff --git a/packages/legacy/core/App/localization/en/index.ts b/packages/legacy/core/App/localization/en/index.ts index 675ccad6a5..0c4fbd22e5 100644 --- a/packages/legacy/core/App/localization/en/index.ts +++ b/packages/legacy/core/App/localization/en/index.ts @@ -414,8 +414,10 @@ const translation = { "ProofRequest": "Proof Request", "RequestProcessing": "Just a moment...", "OfferDelay": "Offer delay", + "ChangeCredential": "Change credential", "RejectThisProof?": "Reject this Proof Request?", "DeclineThisProof?": "Decline this Proof Request?", + "MultipleCredentials": "You have multiple credentials to choose from:", "AcceptingProof": "Accepting Proof", "SuccessfullyAcceptedProof": "Successfully Accepted Proof", "SensitiveInformation": "This request is asking for sensitive information.", @@ -511,6 +513,7 @@ const translation = { "CredentialDetails": "Credential Details", "Notifications": "Notifications", "CredentialOffer": "Credential Offer", + "ProofChangeCredential": "Choose a credential", "ProofRequest": "Proof Request", "ProofRequestDetails": "Proof Request Details", "ProofRequestAttributeDetails": "Proof Request Attribute Details", diff --git a/packages/legacy/core/App/localization/fr/index.ts b/packages/legacy/core/App/localization/fr/index.ts index 91a4582810..611c15bd0a 100644 --- a/packages/legacy/core/App/localization/fr/index.ts +++ b/packages/legacy/core/App/localization/fr/index.ts @@ -412,8 +412,10 @@ const translation = { "ProofRequest": "Demande de preuve", "RequestProcessing": "Juste un instant...", "OfferDelay": "Retard de l'offre", + "ChangeCredential": "Change credential (FR)", "RejectThisProof?": "Rejeter cette preuve?", "AcceptingProof": "Acceptation de la preuve", + "MultipleCredentials": "You have multiple credentials to choose from: (FR)", "SuccessfullyAcceptedProof": "Preuve acceptée avec succès", "SensitiveInformation": "This request is asking for sensitive information. (FR)", "RejectingProof": "Rejet de la preuve", @@ -498,6 +500,7 @@ const translation = { "CredentialDetails": "Détails des justificatifs", "Notifications": "Notifications", "CredentialOffer": "Offre de justificatif", + "ProofChangeCredential":"Choose a credential (FR)", "ProofRequest": "Demande de preuve", "ProofRequestAttributeDetails": "Détails des attributs de la demande de preuve", "ProofDetails": "Détails de la preuve", diff --git a/packages/legacy/core/App/localization/pt-br/index.ts b/packages/legacy/core/App/localization/pt-br/index.ts index 57a39a35ab..74438ce250 100644 --- a/packages/legacy/core/App/localization/pt-br/index.ts +++ b/packages/legacy/core/App/localization/pt-br/index.ts @@ -394,9 +394,11 @@ const translation = { "ProofRequest": "Requisição de Prova", "RequestProcessing": "Só um momento...", "OfferDelay": "Atrasar oferta", + "ChangeCredential": "Escolher credencial", "RejectThisProof?": "Rejeitar esta Requisição de Prova?", "DeclineThisProof?": "Recusar esta Requisição de Prova?", "AcceptingProof": "Aceitando Prova", + "MultipleCredentials": "Você tem múltiplas credenciais para escolher:", "SuccessfullyAcceptedProof": "Prova Aceita com Sucesso", "SensitiveInformation": "This request is asking for sensitive information. (PB)", "ProofRequestNotFound": "Requisição de Prova não encontrada.", @@ -482,6 +484,7 @@ const translation = { "Notifications": "Notificações", "CredentialOffer": "Oferta de Credencial", "ProofRequest": "Requisição de Prova", + "ProofChangeCredential":"Escolha uma credencial", "ProofRequestDetails": "Detalhes Da Solicitação De Comprovação", "ProofRequestAttributeDetails": "Atributos de Requisição de Prova", "ProofDetails": "Detalhes da prova", diff --git a/packages/legacy/core/App/navigators/ProofRequestStack.tsx b/packages/legacy/core/App/navigators/ProofRequestStack.tsx index b169f2a6ce..5f1564980a 100644 --- a/packages/legacy/core/App/navigators/ProofRequestStack.tsx +++ b/packages/legacy/core/App/navigators/ProofRequestStack.tsx @@ -6,6 +6,7 @@ import HeaderButton, { ButtonLocation } from '../components/buttons/HeaderButton import HeaderRightHome from '../components/buttons/HeaderHome' import { useTheme } from '../contexts/theme' import ListProofRequests from '../screens/ListProofRequests' +import ProofChangeCredential from '../screens/ProofChangeCredential' import ProofDetails from '../screens/ProofDetails' import ProofRequestDetails from '../screens/ProofRequestDetails' import ProofRequestUsageHistory from '../screens/ProofRequestUsageHistory' @@ -35,6 +36,11 @@ const ProofRequestStack: React.FC = () => { title: '', })} /> + + +const ProofChangeCredential: React.FC = ({ route, navigation }) => { + if (!route?.params) { + throw new Error('Change credential route params were not set properly') + } + const proofId = route.params.proofId + const selectedCred = route.params.selectedCred + const altCredentials = route.params.altCredentials + const onCredChange = route.params.onCredChange + const { ColorPallet, TextTheme } = useTheme() + const { t } = useTranslation() + const [loading, setLoading] = useState(false) + const [proofItems, setProofItems] = useState([]) + const [retrievedCredentials, setRetrievedCredentials] = useState() + const credProofPromise = useAllCredentialsForProof(proofId) + const styles = StyleSheet.create({ + pageContainer: { + flex: 1, + }, + pageMargin: { + marginHorizontal: 20, + }, + cardLoading: { + backgroundColor: ColorPallet.brand.secondaryBackground, + flex: 1, + flexGrow: 1, + marginVertical: 35, + borderRadius: 15, + paddingHorizontal: 10, + }, + selectedCred: { + borderWidth: 5, + borderRadius: 15, + borderColor: ColorPallet.semantic.focus, + }, + }) + + const getCredentialsFields = (): Fields => ({ + ...retrievedCredentials?.attributes, + ...retrievedCredentials?.predicates, + }) + + useEffect(() => { + setLoading(true) + + credProofPromise + ?.then((value) => { + if (value) { + const { groupedProof, retrievedCredentials } = value + setLoading(false) + const activeCreds = groupedProof.filter((proof) => altCredentials.includes(proof.credId)) + const credList = activeCreds.map((cred) => cred.credId) + const formatCredentials = ( + retrievedItems: Record + ) => { + return Object.keys(retrievedItems) + .map((key) => { + return { + [key]: retrievedItems[key].filter((attr) => credList.includes(attr.credentialId)), + } + }) + .reduce((prev, curr) => { + return { + ...prev, + ...curr, + } + }, {}) + } + const selectRetrievedCredentials: AnonCredsCredentialsForProofRequest | undefined = retrievedCredentials + ? { + ...retrievedCredentials, + attributes: formatCredentials(retrievedCredentials.attributes) as Record< + string, + AnonCredsRequestedAttributeMatch[] + >, + predicates: formatCredentials(retrievedCredentials.predicates) as Record< + string, + AnonCredsRequestedPredicateMatch[] + >, + } + : undefined + setRetrievedCredentials(selectRetrievedCredentials) + setProofItems(activeCreds) + } + }) + .catch((err: unknown) => { + const error = new BifoldError( + t('Error.Title1026'), + t('Error.Message1026'), + (err as Error)?.message ?? err, + 1026 + ) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + }) + }, []) + + const listHeader = () => { + return ( + + {loading ? ( + + + + ) : ( + {t('ProofRequest.MultipleCredentials')} + )} + + ) + } + + const changeCred = (credId: string) => { + onCredChange(credId) + navigation.goBack() + } + const hasSatisfiedPredicates = (fields: Fields, credId?: string) => + proofItems.flatMap((item) => evaluatePredicates(fields, credId)(item)).every((p) => p.satisfied) + + return ( + + { + return ( + + changeCred(item.credId ?? '')} + style={[item.credId === selectedCred ? styles.selectedCred : undefined, { marginBottom: 10 }]} + > + + + + ) + }} + > + + ) +} + +export default ProofChangeCredential diff --git a/packages/legacy/core/App/screens/ProofRequest.tsx b/packages/legacy/core/App/screens/ProofRequest.tsx index d2601a309c..686717474e 100644 --- a/packages/legacy/core/App/screens/ProofRequest.tsx +++ b/packages/legacy/core/App/screens/ProofRequest.tsx @@ -1,20 +1,13 @@ import type { StackScreenProps } from '@react-navigation/stack' import { - AnonCredsCredentialInfo, AnonCredsCredentialsForProofRequest, - AnonCredsPredicateType, AnonCredsRequestedAttributeMatch, AnonCredsRequestedPredicateMatch, } from '@aries-framework/anoncreds' -import { CredentialExchangeRecord, ProofExchangeRecord } from '@aries-framework/core' -import { useConnectionById, useCredentials, useProofById } from '@aries-framework/react-hooks' -import { - Predicate, - ProofCredentialAttributes, - ProofCredentialItems, - ProofCredentialPredicates, -} from '@hyperledger/aries-oca/build/legacy' +import { CredentialExchangeRecord } from '@aries-framework/core' +import { useConnectionById, useProofById } from '@aries-framework/react-hooks' +import { Attribute, Predicate } from '@hyperledger/aries-oca/build/legacy' import { useIsFocused } from '@react-navigation/core' import moment from 'moment' import React, { useEffect, useMemo, useState } from 'react' @@ -37,24 +30,19 @@ import { useStore } from '../contexts/store' import { useTheme } from '../contexts/theme' import { useTour } from '../contexts/tour/tour-context' import { useOutOfBandByConnectionId } from '../hooks/connections' +import { useAllCredentialsForProof } from '../hooks/proofs' import { BifoldError } from '../types/error' -import { NotificationStackParams, Screens, TabStacks } from '../types/navigators' +import { NotificationStackParams, Screens, Stacks, TabStacks } from '../types/navigators' +import { ProofCredentialAttributes, ProofCredentialItems, ProofCredentialPredicates } from '../types/proof-items' import { ModalUsage } from '../types/remove' import { TourID } from '../types/tour' import { useAppAgent } from '../utils/agent' -import { getCredentialIdentifiers } from '../utils/credential' -import { - getConnectionName, - mergeAttributesAndPredicates, - processProofAttributes, - processProofPredicates, -} from '../utils/helpers' +import { getConnectionName, Fields, evaluatePredicates, getCredentialInfo } from '../utils/helpers' import { testIdWithKey } from '../utils/testable' import ProofRequestAccept from './ProofRequestAccept' type ProofRequestProps = StackScreenProps -type Fields = Record const ProofRequest: React.FC = ({ navigation, route }) => { if (!route?.params) { @@ -65,13 +53,11 @@ const ProofRequest: React.FC = ({ navigation, route }) => { const { agent } = useAppAgent() const { t } = useTranslation() const { assertConnectedNetwork } = useNetwork() - const fullCredentials = useCredentials().records const proof = useProofById(proofId) const connection = proof?.connectionId ? useConnectionById(proof.connectionId) : undefined const [pendingModalVisible, setPendingModalVisible] = useState(false) const [revocationOffense, setRevocationOffense] = useState(false) const [retrievedCredentials, setRetrievedCredentials] = useState() - const [proofItems, setProofItems] = useState([]) const [loading, setLoading] = useState(true) const [declineModalVisible, setDeclineModalVisible] = useState(false) const { ColorPallet, ListItems, TextTheme } = useTheme() @@ -79,7 +65,10 @@ const ProofRequest: React.FC = ({ navigation, route }) => { const goalCode = useOutOfBandByConnectionId(proof?.connectionId ?? '')?.outOfBandInvitation.goalCode const { enableTours: enableToursConfig, OCABundleResolver } = useConfiguration() const [containsPI, setContainsPI] = useState(false) + const [activeCreds, setActiveCreds] = useState([]) + const [selectedCredentials, setSelectedCredentials] = useState([]) const [store, dispatch] = useStore() + const credProofPromise = useAllCredentialsForProof(proofId) const proofConnectionLabel = useMemo( () => getConnectionName(connection, store.preferences.alternateContactNames), [connection, store.preferences.alternateContactNames] @@ -172,7 +161,7 @@ const ProofRequest: React.FC = ({ navigation, route }) => { const containsRevokedCreds = ( credExRecords: CredentialExchangeRecord[], fields: { - [key: string]: ProofCredentialAttributes & ProofCredentialPredicates + [key: string]: Attribute[] & Predicate[] } ) => { const revList = credExRecords.map((cred) => { @@ -186,111 +175,95 @@ const ProofRequest: React.FC = ({ navigation, route }) => { const revDate = moment(item.revocationDate) return item.id.some((id) => { return Object.keys(fields).some((key) => { - const dateIntervals = ((fields[key].attributes ?? fields[key].predicates) as any[]) - ?.filter((attr: any) => attr.credentialId === id) - .map((attr: any) => { + const dateIntervals = fields[key] + ?.filter((attr) => attr.credentialId === id) + .map((attr) => { return { to: attr.nonRevoked?.to !== undefined ? moment.unix(attr.nonRevoked.to) : undefined, from: attr.nonRevoked?.from !== undefined ? moment.unix(attr.nonRevoked.from) : undefined, } }) return dateIntervals?.some( - (inter: any) => + (inter) => (inter.to !== undefined && inter.to > revDate) || (inter.from !== undefined && inter.from > revDate) ) }) }) }) } - useMemo(() => { - if (!(agent && proof)) { - return - } + useEffect(() => { setLoading(true) + credProofPromise + ?.then((value) => { + if (value) { + const { groupedProof, retrievedCredentials, fullCredentials } = value + setLoading(false) + let credList: string[] = [] + if (selectedCredentials.length > 0) { + credList = selectedCredentials + } else { + // we only want one of each satisfying credential + groupedProof.forEach((item) => { + const credId = item.altCredentials?.[0] + if (credId && !credList.includes(credId)) { + credList.push(credId) + } + }) + } - const retrieveCredentialsForProof = async (proof: ProofExchangeRecord) => { - try { - const format = await agent.proofs.getFormatData(proof.id) - const hasAnonCreds = format.request?.anoncreds !== undefined - const hasIndy = format.request?.indy !== undefined - const credentials = await agent.proofs.getCredentialsForRequest({ - proofRecordId: proof.id, - proofFormats: { - // FIXME: AFJ will try to use the format, even if the value is undefined (but the key is present) - // We should ignore the key, if the value is undefined. For now this is a workaround. - ...(hasIndy - ? { - indy: { - // Setting `filterByNonRevocationRequirements` to `false` returns all - // credentials even if they are revokable (and revoked). We need this to - // be able to show why a proof cannot be satisfied. Otherwise we can only - // show failure. - filterByNonRevocationRequirements: false, - }, + const formatCredentials = ( + retrievedItems: Record, + credList: string[] + ) => { + return Object.keys(retrievedItems) + .map((key) => { + return { + [key]: retrievedItems[key].filter((attr) => credList.includes(attr.credentialId)), } - : {}), - - ...(hasAnonCreds - ? { - anoncreds: { - // Setting `filterByNonRevocationRequirements` to `false` returns all - // credentials even if they are revokable (and revoked). We need this to - // be able to show why a proof cannot be satisfied. Otherwise we can only - // show failure. - filterByNonRevocationRequirements: false, - }, + }) + .reduce((prev, curr) => { + return { + ...prev, + ...curr, } - : {}), - }, - }) - if (!credentials) { - throw new Error(t('ProofRequest.RequestedCredentialsCouldNotBeFound')) - } + }, {}) + } - if (!format) { - throw new Error(t('ProofRequest.RequestedCredentialsCouldNotBeFound')) - } - return { format, credentials } - } catch (err: unknown) { - const error = new BifoldError( - t('Error.Title1043'), - t('Error.Message1043'), - (err as Error)?.message ?? err, - 1043 - ) - DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) - } - } + const selectRetrievedCredentials: AnonCredsCredentialsForProofRequest | undefined = retrievedCredentials + ? { + ...retrievedCredentials, + attributes: formatCredentials(retrievedCredentials.attributes, credList) as Record< + string, + AnonCredsRequestedAttributeMatch[] + >, + predicates: formatCredentials(retrievedCredentials.predicates, credList) as Record< + string, + AnonCredsRequestedPredicateMatch[] + >, + } + : undefined + setRetrievedCredentials(selectRetrievedCredentials) + + const activeCreds = groupedProof.filter((item) => credList.includes(item.credId)) + setActiveCreds(activeCreds) + + const unpackCredToField = ( + credentials: (ProofCredentialAttributes & ProofCredentialPredicates)[] + ): { [key: string]: Attribute[] & Predicate[] } => { + return credentials.reduce((prev, current) => { + return { ...prev, [current.credId]: current.attributes ?? current.predicates ?? [] } + }, {}) + } - retrieveCredentialsForProof(proof) - .then((retrieved) => retrieved ?? { format: undefined, credentials: undefined }) - .then(({ format, credentials }) => { - if (!(format && credentials && fullCredentials)) { - return + const records = fullCredentials.filter((record) => + record.credentials.some((cred) => credList.includes(cred.credentialRecordId)) + ) + const foundRevocationOffense = + containsRevokedCreds(records, unpackCredToField(activeCreds)) || + containsRevokedCreds(records, unpackCredToField(activeCreds)) + setRevocationOffense(foundRevocationOffense) } - - const proofFormat = credentials.proofFormats.anoncreds ?? credentials.proofFormats.indy - const reqCredIds = [ - ...Object.keys(proofFormat?.attributes ?? {}).map((key) => proofFormat?.attributes[key][0]?.credentialId), - ...Object.keys(proofFormat?.predicates ?? {}).map((key) => proofFormat?.predicates[key][0]?.credentialId), - ] - const credentialRecords = fullCredentials.filter((record) => - reqCredIds.includes(record.credentials[0]?.credentialRecordId) - ) - const attributes = processProofAttributes(format.request, credentials, credentialRecords) - const predicates = processProofPredicates(format.request, credentials, credentialRecords) - - setRetrievedCredentials(proofFormat) - - const foundRevocationOffense = - containsRevokedCreds(credentialRecords, attributes) || containsRevokedCreds(credentialRecords, predicates) - - setRevocationOffense(foundRevocationOffense) - - const groupedProof = Object.values(mergeAttributesAndPredicates(attributes, predicates)) - setProofItems(groupedProof) - setLoading(false) }) .catch((err: unknown) => { const error = new BifoldError( @@ -301,7 +274,7 @@ const ProofRequest: React.FC = ({ navigation, route }) => { ) DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) }) - }, []) + }, [selectedCredentials]) const toggleDeclineModalVisible = () => setDeclineModalVisible(!declineModalVisible) @@ -312,111 +285,32 @@ const ProofRequest: React.FC = ({ navigation, route }) => { useEffect(() => { // get oca bundle to see if we're presenting personally identifiable elements - proofItems.some(async (item) => { - if (!item || !item.credExchangeRecord) { + activeCreds.some(async (item) => { + if (!item || !(item.credDefId || item.schemaId)) { return false } const labels = (item.attributes ?? []).map((field) => field.label ?? field.name ?? '') - const credIds = getCredentialIdentifiers(item.credExchangeRecord) - const bundle = await OCABundleResolver.resolveAllBundles({ identifiers: credIds }) + const bundle = await OCABundleResolver.resolveAllBundles({ + identifiers: { credentialDefinitionId: item.credDefId, schemaId: item.schemaId }, + }) const flaggedAttributes: string[] = (bundle as any).bundle.bundle.flaggedAttributes.map((attr: any) => attr.name) const foundPI = labels.some((label) => flaggedAttributes.includes(label)) setContainsPI(foundPI) return foundPI }) - }, [proofItems]) - - /** - * Retrieve current credentials info filtered by `credentialDefinitionId` if given. - * @param credDefId Credential Definition Id - * @returns Array of `AnonCredsCredentialInfo` - */ - const getCredentialInfo = (credDefId?: string): AnonCredsCredentialInfo[] => { - const credentialInfo: AnonCredsCredentialInfo[] = [] - const fields = getCredentialsFields() - - Object.keys(fields).forEach((proofKey) => { - credentialInfo.push(...fields[proofKey].map((attr) => attr.credentialInfo)) - }) + }, [activeCreds]) - return credDefId == undefined - ? credentialInfo - : credentialInfo.filter((cred) => cred.credentialDefinitionId === credDefId) - } - - /** - * Evaluate if given attribute value satisfies the predicate. - * @param attribute Credential attribute value - * @param pValue Predicate value - * @param pType Predicate type ({@link AnonCredsPredicateType}) - * @returns `true`if predicate is satisfied, otherwise `false` - */ - const evaluateOperation = (attribute: number, pValue: number, pType: AnonCredsPredicateType): boolean => { - if (pType === '>=') { - return attribute >= pValue - } - - if (pType === '>') { - return attribute > pValue - } - - if (pType === '<=') { - return attribute <= pValue - } - if (pType === '<') { - return attribute < pValue - } - - return false - } - - /** - * Given proof credential items, evaluate and return its predicates, setting `satisfied` property. - * @param proofCredentialsItems - * @returns Array of evaluated predicates - */ - const evaluatePredicates = - (credDefId?: string) => - (proofCredentialItems: ProofCredentialItems): Predicate[] => { - const predicates = proofCredentialItems.predicates - - if (!predicates || predicates.length == 0) { - return [] - } - - if (credDefId && credDefId != proofCredentialItems.credDefId) { - return [] - } - - const credentialAttributes = getCredentialInfo(proofCredentialItems.credDefId).map((ci) => ci.attributes) - - return predicates.map((predicate) => { - const { pType: pType, pValue: pValue, name: field } = predicate - let satisfied = false - - if (field) { - const attribute = (credentialAttributes.find((attr) => attr[field] != undefined) ?? {})[field] - - if (attribute && pValue) { - satisfied = evaluateOperation(Number(attribute), Number(pValue), pType as AnonCredsPredicateType) - } - } - - return { ...predicate, satisfied } - }) - } - - const hasAvailableCredentials = (credDefId?: string): boolean => { + const hasAvailableCredentials = (credId?: string): boolean => { const fields = getCredentialsFields() - if (credDefId) { - return getCredentialInfo(credDefId).some((credInfo) => credInfo.credentialDefinitionId === credDefId) + if (credId) { + return getCredentialInfo(credId, fields).some((credInfo) => credInfo.credentialId === credId) } return !!retrievedCredentials && Object.values(fields).every((c) => c.length > 0) } - const hasSatisfiedPredicates = (credDefId?: string) => - proofItems.flatMap(evaluatePredicates(credDefId)).every((p) => p.satisfied) + const hasSatisfiedPredicates = (fields: Fields, credId?: string) => + activeCreds.flatMap((item) => evaluatePredicates(fields, credId)(item)).every((p) => p.satisfied) const handleAcceptPress = async () => { try { @@ -432,14 +326,35 @@ const ProofRequest: React.FC = ({ navigation, route }) => { const formatToUse = format.request?.anoncreds ? 'anoncreds' : 'indy' - const automaticRequestedCreds = - retrievedCredentials && - (await agent.proofs.selectCredentialsForRequest({ - proofRecordId: proof.id, - proofFormats: { - [formatToUse]: {}, - }, - })) + const formatCredentials = ( + retrievedItems: Record, + credList: string[] + ) => { + return Object.keys(retrievedItems) + .map((key) => { + return { + [key]: retrievedItems[key].find((cred) => credList.includes(cred.credentialId)), + } + }) + .reduce((prev, current) => { + return { ...prev, ...current } + }, {}) + } + + // this is the best way to supply our desired credentials in the proof, otherwise it selects them automatically + const credObject = { + ...retrievedCredentials, + attributes: formatCredentials( + retrievedCredentials.attributes, + activeCreds.map((item) => item.credId) + ), + predicates: formatCredentials( + retrievedCredentials.predicates, + activeCreds.map((item) => item.credId) + ), + selfAttestedAttributes: {}, + } + const automaticRequestedCreds = { proofFormats: { [formatToUse]: { ...credObject } } } if (!automaticRequestedCreds) { throw new Error(t('ProofRequest.RequestedCredentialsCouldNotBeFound')) @@ -492,7 +407,7 @@ const ProofRequest: React.FC = ({ navigation, route }) => { <> - {!hasAvailableCredentials() || !hasSatisfiedPredicates() ? ( + {!hasAvailableCredentials() || !hasSatisfiedPredicates(getCredentialsFields()) ? ( = ({ navigation, route }) => { color={ListItems.proofIcon.color} size={ListItems.proofIcon.fontSize} /> - {hasSatisfiedPredicates() ? ( + {hasSatisfiedPredicates(getCredentialsFields()) ? ( {proofConnectionLabel || t('ContactDetails.AContact')}{' '} {t('ProofRequest.IsRequestingSomethingYouDontHaveAvailable')} @@ -516,8 +431,8 @@ const ProofRequest: React.FC = ({ navigation, route }) => { {proofConnectionLabel || t('ContactDetails.AContact')}{' '} {t('ProofRequest.IsRequestingYouToShare')} - {` ${proofItems.length} `} - {proofItems.length > 1 ? t('ProofRequest.Credentials') : t('ProofRequest.Credential')} + {` ${activeCreds?.length} `} + {activeCreds?.length > 1 ? t('ProofRequest.Credentials') : t('ProofRequest.Credential')} )} {containsPI && ( @@ -558,6 +473,24 @@ const ProofRequest: React.FC = ({ navigation, route }) => { ) } + const handleAltCredChange = (selectedCred: string, altCredentials: string[]) => { + const onCredChange = (cred: string) => { + const newSelectedCreds = ( + selectedCredentials.length > 0 ? selectedCredentials : activeCreds.map((item) => item.credId) + ).filter((id) => !altCredentials.includes(id)) + setSelectedCredentials([cred, ...newSelectedCreds]) + } + navigation.getParent()?.navigate(Stacks.ProofRequestsStack, { + screen: Screens.ProofChangeCredential, + params: { + selectedCred, + altCredentials, + proofId, + onCredChange, + }, + }) + } + const proofPageFooter = () => { return ( @@ -571,7 +504,9 @@ const ProofRequest: React.FC = ({ navigation, route }) => { testID={testIdWithKey('Share')} buttonType={ButtonType.Primary} onPress={handleAcceptPress} - disabled={!hasAvailableCredentials() || !hasSatisfiedPredicates() || revocationOffense} + disabled={ + !hasAvailableCredentials() || !hasSatisfiedPredicates(getCredentialsFields()) || revocationOffense + } /> @@ -586,26 +521,42 @@ const ProofRequest: React.FC = ({ navigation, route }) => { ) } + // FIXME: (WK) we need to have all creds in the cred list otherwise react will complain that the order of hooks changes. Solution it to add all creds to flatlist but only display selection return ( { return ( - - + + {loading ? null : ( + + 1} + handleAltCredChange={ + item.altCredentials && item.altCredentials.length > 1 + ? () => { + handleAltCredChange(item.credId, item.altCredentials ?? [item.credId]) + } + : undefined + } + proof={true} + > + + )} ) }} diff --git a/packages/legacy/core/App/types/navigators.ts b/packages/legacy/core/App/types/navigators.ts index d9e97164a1..a093da6c4c 100644 --- a/packages/legacy/core/App/types/navigators.ts +++ b/packages/legacy/core/App/types/navigators.ts @@ -30,6 +30,7 @@ export enum Screens { UseBiometry = 'Use Biometry', Developer = 'Developer', CustomNotification = 'Custom Notification', + ProofChangeCredential = 'Choose a credential', ProofRequests = 'Proof Requests', ProofRequesting = 'Proof Requesting', ProofDetails = 'Proof Details', @@ -105,6 +106,12 @@ export type ProofRequestsStackParams = { [Screens.ProofDetails]: { recordId: string; isHistory?: boolean; senderReview?: boolean } [Screens.ProofRequestDetails]: { templateId: string; connectionId?: string } [Screens.ProofRequestUsageHistory]: { templateId: string } + [Screens.ProofChangeCredential]: { + selectedCred: string + altCredentials: string[] + proofId: string + onCredChange: (arg: string) => void + } } export type CredentialStackParams = { diff --git a/packages/legacy/core/App/types/proof-items.ts b/packages/legacy/core/App/types/proof-items.ts new file mode 100644 index 0000000000..99558263a3 --- /dev/null +++ b/packages/legacy/core/App/types/proof-items.ts @@ -0,0 +1,24 @@ +import { CredentialExchangeRecord } from '@aries-framework/core' +import { Attribute, Predicate } from '@hyperledger/aries-oca/build/legacy' + +export interface ProofCredentialAttributes { + altCredentials?: string[] + credExchangeRecord?: CredentialExchangeRecord + credId: string + credDefId?: string + schemaId?: string + credName: string + attributes?: Attribute[] +} + +export interface ProofCredentialPredicates { + altCredentials?: string[] + credExchangeRecord?: CredentialExchangeRecord + credId: string + credDefId?: string + schemaId?: string + credName: string + predicates?: Predicate[] +} + +export interface ProofCredentialItems extends ProofCredentialAttributes, ProofCredentialPredicates {} diff --git a/packages/legacy/core/App/utils/helpers.ts b/packages/legacy/core/App/utils/helpers.ts index c18465f962..34871cbfd9 100644 --- a/packages/legacy/core/App/utils/helpers.ts +++ b/packages/legacy/core/App/utils/helpers.ts @@ -1,9 +1,13 @@ import { + AnonCredsCredentialInfo, AnonCredsCredentialsForProofRequest, + AnonCredsPredicateType, AnonCredsProofFormat, AnonCredsProofFormatService, AnonCredsProofRequestRestriction, + AnonCredsRequestedAttribute, AnonCredsRequestedAttributeMatch, + AnonCredsRequestedPredicate, AnonCredsRequestedPredicateMatch, LegacyIndyProofFormat, LegacyIndyProofFormatService, @@ -23,23 +27,23 @@ import { ProofFormatDataMessagePayload, } from '@aries-framework/core/build/modules/proofs/protocol/ProofProtocolOptions' import { useConnectionById } from '@aries-framework/react-hooks' -import { - Attribute, - Predicate, - ProofCredentialAttributes, - ProofCredentialPredicates, -} from '@hyperledger/aries-oca/build/legacy' +import { Attribute, Predicate } from '@hyperledger/aries-oca/build/legacy' import { Buffer } from 'buffer' import moment from 'moment' import { ParsedUrl, parseUrl } from 'query-string' import { Dispatch, ReactNode, SetStateAction } from 'react' +import { TFunction } from 'react-i18next' +import { DeviceEventEmitter } from 'react-native' -import { domain } from '../constants' +import { EventTypes, domain } from '../constants' import { i18n } from '../localization/index' import { Role } from '../types/chat' +import { BifoldError } from '../types/error' +import { ProofCredentialAttributes, ProofCredentialItems, ProofCredentialPredicates } from '../types/proof-items' import { ChildFn } from '../types/tour' export { parsedCredDefNameFromCredential } from './cred-def' +import { BifoldAgent } from './agent' import { parseCredDefFromId } from './cred-def' export { parsedCredDefName } from './cred-def' @@ -335,7 +339,109 @@ const credNameFromRestriction = (queries?: AnonCredsProofRequestRestriction[]): export const isDataUrl = (value: string | number | null) => { return typeof value === 'string' && value.startsWith('data:image/') } +export type Fields = Record + +/** + * Retrieve current credentials info filtered by `credentialDefinitionId` if given. + * @param credDefId Credential Definition Id + * @returns Array of `AnonCredsCredentialInfo` + */ +export const getCredentialInfo = (credId: string, fields: Fields): AnonCredsCredentialInfo[] => { + const credentialInfo: AnonCredsCredentialInfo[] = [] + + Object.keys(fields).forEach((proofKey) => { + credentialInfo.push(...fields[proofKey].map((attr) => attr.credentialInfo)) + }) + + return !credId ? credentialInfo : credentialInfo.filter((cred) => cred.credentialId === credId) +} + +/** + * Evaluate if given attribute value satisfies the predicate. + * @param attribute Credential attribute value + * @param pValue Predicate value + * @param pType Predicate type ({@link AnonCredsPredicateType}) + * @returns `true`if predicate is satisfied, otherwise `false` + */ +const evaluateOperation = (attribute: number, pValue: number, pType: AnonCredsPredicateType): boolean => { + if (pType === '>=') { + return attribute >= pValue + } + + if (pType === '>') { + return attribute > pValue + } + + if (pType === '<=') { + return attribute <= pValue + } + if (pType === '<') { + return attribute < pValue + } + + return false +} + +/** + * Given proof credential items, evaluate and return its predicates, setting `satisfied` property. + * @param proofCredentialsItems + * @returns Array of evaluated predicates + */ +export const evaluatePredicates = + (fields: Fields, credId?: string) => + (proofCredentialItems: ProofCredentialItems): Predicate[] => { + const predicates = proofCredentialItems.predicates + if (!predicates || predicates.length == 0) { + return [] + } + + if ((credId && credId != proofCredentialItems.credId) || !proofCredentialItems.credId) { + return [] + } + + const credentialAttributes = getCredentialInfo(proofCredentialItems.credId, fields).map((ci) => ci.attributes) + + return predicates.map((predicate: Predicate) => { + const { pType: pType, pValue: pValue, name: field } = predicate + let satisfied = false + + if (field) { + const attribute = (credentialAttributes.find((attr) => attr[field] != undefined) ?? {})[field] + + if (attribute && pValue) { + satisfied = evaluateOperation(Number(attribute), Number(pValue), pType as AnonCredsPredicateType) + } + } + + return { ...predicate, satisfied } + }) + } +const addMissingDisplayAttributes = (attrReq: AnonCredsRequestedAttribute) => { + const credName = credNameFromRestriction(attrReq.restrictions) + //there is no credId in this context so use credName as a placeholder + const processedAttributes: ProofCredentialAttributes = { + credExchangeRecord: undefined, + altCredentials: [credName], + credId: credName, + schemaId: undefined, + credDefId: undefined, + credName: credName, + attributes: [] as Attribute[], + } + const { name, names } = attrReq + for (const attributeName of [...(names ?? (name && [name]) ?? [])]) { + processedAttributes.attributes?.push( + new Attribute({ + revoked: false, + credentialId: credName, + name: attributeName, + value: '', + }) + ) + } + return processedAttributes +} export const processProofAttributes = ( request?: ProofFormatDataMessagePayload<[LegacyIndyProofFormat, AnonCredsProofFormat], 'request'> | undefined, credentials?: GetCredentialsForRequestReturn<[LegacyIndyProofFormatService, AnonCredsProofFormatService]>, @@ -347,62 +453,128 @@ export const processProofAttributes = ( const retrievedCredentialAttributes = credentials?.proofFormats?.indy?.attributes ?? credentials?.proofFormats?.anoncreds?.attributes + // non_revoked interval can sometimes be top level + const requestNonRevoked = request?.indy?.non_revoked ?? request?.anoncreds?.non_revoked + if (!requestedProofAttributes || !retrievedCredentialAttributes) { return {} } - for (const key of Object.keys(retrievedCredentialAttributes)) { - // The shift operation modifies the original input array, therefore make a copy - const credential = [...(retrievedCredentialAttributes[key] ?? [])].sort(credentialSortFn).shift() - const credNameRestriction = credNameFromRestriction(requestedProofAttributes[key]?.restrictions) - - let credName = credNameRestriction ?? key - if (credential?.credentialInfo?.credentialDefinitionId || credential?.credentialInfo?.schemaId) { - credName = parseCredDefFromId( - credential?.credentialInfo?.credentialDefinitionId, - credential?.credentialInfo?.schemaId - ) - } - let revoked = false - let credExchangeRecord = undefined - if (credential) { - credExchangeRecord = credentialRecords?.filter( - (record) => record.credentials[0]?.credentialRecordId === credential.credentialId - )[0] - revoked = credExchangeRecord?.revocationNotification !== undefined - } + const altCredentials = [...(retrievedCredentialAttributes[key] ?? [])] + .sort(credentialSortFn) + .map((cred) => cred.credentialId) + + const credentialList = [...(retrievedCredentialAttributes[key] ?? [])].sort(credentialSortFn) + const { name, names, non_revoked } = requestedProofAttributes[key] - for (const attributeName of [...(names ?? (name && [name]) ?? [])]) { - if (!processedAttributes[credName]) { - // init processedAttributes object - processedAttributes[credName] = { - credExchangeRecord, - schemaId: credential?.credentialInfo?.schemaId, - credDefId: credential?.credentialInfo?.credentialDefinitionId, - credName, - attributes: [], - } + if (credentialList.length <= 0) { + const missingAttributes = addMissingDisplayAttributes(requestedProofAttributes[key]) + if (!processedAttributes[key]) { + processedAttributes[key] = missingAttributes + } else { + processedAttributes[key].attributes?.push(...(missingAttributes.attributes ?? [])) } + } - let attributeValue = '' + //iterate over all credentials that satisfy the proof + for (const credential of credentialList) { + let credName = key + if (credential?.credentialInfo?.credentialDefinitionId || credential?.credentialInfo?.schemaId) { + credName = parseCredDefFromId( + credential?.credentialInfo?.credentialDefinitionId, + credential?.credentialInfo?.schemaId + ) + } + let revoked = false + let credExchangeRecord = undefined if (credential) { - attributeValue = credential.credentialInfo.attributes[attributeName] + credExchangeRecord = credentialRecords?.find((record) => + record.credentials.map((cred) => cred.credentialRecordId).includes(credential.credentialId) + ) + revoked = credExchangeRecord?.revocationNotification !== undefined + } else { + continue + } + + for (const attributeName of [...(names ?? (name && [name]) ?? [])]) { + if (!processedAttributes[credential?.credentialId]) { + // init processedAttributes object + processedAttributes[credential.credentialId] = { + credExchangeRecord, + altCredentials, + credId: credential?.credentialId, + schemaId: credential?.credentialInfo?.schemaId, + credDefId: credential?.credentialInfo?.credentialDefinitionId, + credName, + attributes: [], + } + } + + let attributeValue = '' + if (credential) { + attributeValue = credential.credentialInfo.attributes[attributeName] + } + processedAttributes[credential.credentialId].attributes?.push( + new Attribute({ + revoked, + credentialId: credential.credentialId, + name: attributeName, + value: attributeValue, + nonRevoked: requestNonRevoked ?? non_revoked, + }) + ) } - processedAttributes[credName].attributes?.push( - new Attribute({ - revoked, - credentialId: credential?.credentialId, - name: attributeName, - value: attributeValue, - nonRevoked: non_revoked, - }) - ) } } return processedAttributes } +export const mergeAttributesAndPredicates = ( + attributes: { [key: string]: ProofCredentialAttributes }, + predicates: { [key: string]: ProofCredentialPredicates } +) => { + const merged: { [key: string]: ProofCredentialAttributes & ProofCredentialPredicates } = { ...attributes } + for (const [key, predicate] of Object.entries(predicates)) { + const existingEntry = merged[key] + if (existingEntry) { + const mergedAltCreds = existingEntry.altCredentials?.filter((credId: string) => + predicate.altCredentials?.includes(credId) + ) + merged[key] = { ...existingEntry, ...predicate } + merged[key].altCredentials = mergedAltCreds + } else { + merged[key] = predicate + } + } + return merged +} + +const addMissingDisplayPredicates = (predReq: AnonCredsRequestedPredicate) => { + const credName = credNameFromRestriction(predReq.restrictions) + //there is no credId in this context so use credName as a placeholder + const processedPredicates: ProofCredentialPredicates = { + credExchangeRecord: undefined, + altCredentials: [credName], + credId: credName, + schemaId: undefined, + credDefId: undefined, + credName: credName, + predicates: [] as Predicate[], + } + const { name, p_type: pType, p_value: pValue } = predReq + + processedPredicates.predicates?.push( + new Predicate({ + revoked: false, + credentialId: credName, + name: name, + pValue, + pType, + }) + ) + return processedPredicates +} export const processProofPredicates = ( request?: ProofFormatDataMessagePayload<[LegacyIndyProofFormat, AnonCredsProofFormat], 'request'> | undefined, credentials?: GetCredentialsForRequestReturn<[LegacyIndyProofFormatService, AnonCredsProofFormatService]>, @@ -418,68 +590,138 @@ export const processProofPredicates = ( return {} } - for (const key of Object.keys(requestedProofPredicates)) { - // The shift operation modifies the original input array, therefore make a copy - const credential = [...(retrievedCredentialPredicates[key] ?? [])].sort(credentialSortFn).shift() - let credExchangeRecord = undefined - if (credential) { - credExchangeRecord = credentialRecords?.filter( - (record) => record.credentials[0]?.credentialRecordId === credential.credentialId - )[0] - } - const { credentialId, credentialDefinitionId, schemaId } = { ...credential, ...credential?.credentialInfo } - const revoked = - credentialRecords?.filter((record) => record.credentials[0]?.credentialRecordId === credentialId)[0] - ?.revocationNotification !== undefined - const { name, p_type: pType, p_value: pValue } = requestedProofPredicates[key] - - const credNameRestriction = credNameFromRestriction(requestedProofPredicates[key]?.restrictions) - - let credName = credNameRestriction ?? key - if (credential?.credentialInfo?.credentialDefinitionId || credential?.credentialInfo?.schemaId) { - credName = parseCredDefFromId( - credential?.credentialInfo?.credentialDefinitionId, - credential?.credentialInfo?.schemaId - ) + // non_revoked interval can sometimes be top level + const requestNonRevoked = request?.indy?.non_revoked ?? request?.anoncreds?.non_revoked + + for (const key of Object.keys(retrievedCredentialPredicates)) { + const altCredentials = [...(retrievedCredentialPredicates[key] ?? [])] + .sort(credentialSortFn) + .map((cred) => cred.credentialId) + + const credentialList = [...(retrievedCredentialPredicates[key] ?? [])].sort(credentialSortFn) + const { name, p_type: pType, p_value: pValue, non_revoked } = requestedProofPredicates[key] + if (credentialList.length <= 0) { + const missingPredicates = addMissingDisplayPredicates(requestedProofPredicates[key]) + if (!processedPredicates[key]) { + processedPredicates[key] = missingPredicates + } else { + processedPredicates[key].predicates?.push(...(missingPredicates.predicates ?? [])) + } } - if (!processedPredicates[credName]) { - processedPredicates[credName] = { - credExchangeRecord, - schemaId, - credDefId: credentialDefinitionId, - credName: credName, - predicates: [], + for (const credential of credentialList) { + let revoked = false + let credExchangeRecord = undefined + if (credential) { + credExchangeRecord = credentialRecords?.find((record) => + record.credentials.map((cred) => cred.credentialRecordId).includes(credential.credentialId) + ) + revoked = credExchangeRecord?.revocationNotification !== undefined + } else { + continue } - } + const { credentialDefinitionId, schemaId } = { ...credential, ...credential?.credentialInfo } - processedPredicates[credName].predicates?.push( - new Predicate({ - credentialId, - name, - revoked, - pValue, - pType, - }) - ) + const credNameRestriction = credNameFromRestriction(requestedProofPredicates[key]?.restrictions) + + let credName = credNameRestriction ?? key + if (credential?.credentialInfo?.credentialDefinitionId || credential?.credentialInfo?.schemaId) { + credName = parseCredDefFromId( + credential?.credentialInfo?.credentialDefinitionId, + credential?.credentialInfo?.schemaId + ) + } + + if (!processedPredicates[credential.credentialId]) { + processedPredicates[credential.credentialId] = { + altCredentials, + credExchangeRecord, + credId: credential.credentialId, + schemaId, + credDefId: credentialDefinitionId, + credName: credName, + predicates: [], + } + } + + processedPredicates[credential.credentialId].predicates?.push( + new Predicate({ + credentialId: credential?.credentialId, + name, + revoked, + pValue, + pType, + nonRevoked: requestNonRevoked ?? non_revoked, + }) + ) + } } return processedPredicates } -export const mergeAttributesAndPredicates = ( - attributes: { [key: string]: ProofCredentialAttributes }, - predicates: { [key: string]: ProofCredentialPredicates } +export const retrieveCredentialsForProof = async ( + agent: BifoldAgent, + proof: ProofExchangeRecord, + fullCredentials: CredentialExchangeRecord[], + t: TFunction<'translation', undefined> ) => { - const merged = { ...attributes } - for (const [key, predicate] of Object.entries(predicates)) { - const existingEntry = merged[key] - if (existingEntry) { - merged[key] = { ...existingEntry, ...predicate } - } else { - merged[key] = predicate + try { + const format = await agent.proofs.getFormatData(proof.id) + const hasAnonCreds = format.request?.anoncreds !== undefined + const hasIndy = format.request?.indy !== undefined + const credentials = await agent.proofs.getCredentialsForRequest({ + proofRecordId: proof.id, + proofFormats: { + // FIXME: AFJ will try to use the format, even if the value is undefined (but the key is present) + // We should ignore the key, if the value is undefined. For now this is a workaround. + ...(hasIndy + ? { + indy: { + // Setting `filterByNonRevocationRequirements` to `false` returns all + // credentials even if they are revokable (and revoked). We need this to + // be able to show why a proof cannot be satisfied. Otherwise we can only + // show failure. + filterByNonRevocationRequirements: false, + }, + } + : {}), + + ...(hasAnonCreds + ? { + anoncreds: { + // Setting `filterByNonRevocationRequirements` to `false` returns all + // credentials even if they are revokable (and revoked). We need this to + // be able to show why a proof cannot be satisfied. Otherwise we can only + // show failure. + filterByNonRevocationRequirements: false, + }, + } + : {}), + }, + }) + if (!credentials) { + throw new Error(t('ProofRequest.RequestedCredentialsCouldNotBeFound')) + } + + if (!format) { + throw new Error(t('ProofRequest.RequestedCredentialsCouldNotBeFound')) + } + + if (!(format && credentials && fullCredentials)) { + return } + + const proofFormat = credentials.proofFormats.anoncreds ?? credentials.proofFormats.indy + + const attributes = processProofAttributes(format.request, credentials, fullCredentials) + const predicates = processProofPredicates(format.request, credentials, fullCredentials) + + const groupedProof = Object.values(mergeAttributesAndPredicates(attributes, predicates)) + return { groupedProof: groupedProof, retrievedCredentials: proofFormat, fullCredentials } + } catch (err: unknown) { + const error = new BifoldError(t('Error.Title1043'), t('Error.Message1043'), (err as Error)?.message ?? err, 1043) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) } - return merged } /** diff --git a/packages/legacy/core/__tests__/screens/ProofChangeCredential.test.tsx b/packages/legacy/core/__tests__/screens/ProofChangeCredential.test.tsx new file mode 100644 index 0000000000..7003954b7d --- /dev/null +++ b/packages/legacy/core/__tests__/screens/ProofChangeCredential.test.tsx @@ -0,0 +1,284 @@ +import { INDY_PROOF_REQUEST_ATTACHMENT_ID, V1RequestPresentationMessage } from '@aries-framework/anoncreds' +import { CredentialExchangeRecord, CredentialState, ProofExchangeRecord, ProofState } from '@aries-framework/core' +import { Attachment, AttachmentData } from '@aries-framework/core/build/decorators/attachment/Attachment' +import { useAgent, useProofById } from '@aries-framework/react-hooks' +import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock' +import { useNavigation } from '@react-navigation/core' +import '@testing-library/jest-native/extend-expect' +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react-native' +import React from 'react' + +import { ConfigurationContext } from '../../App/contexts/configuration' +import { NetworkProvider } from '../../App/contexts/network' +import { testIdWithKey } from '../../App/utils/testable' +import configurationContext from '../contexts/configuration' +import timeTravel from '../helpers/timetravel' +import ProofChangeCredential from '../../App/screens/ProofChangeCredential' + +jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') +jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo) +jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper') +jest.mock('@react-navigation/core', () => { + return require('../../__mocks__/custom/@react-navigation/core') +}) +jest.mock('@react-navigation/native', () => { + return require('../../__mocks__/custom/@react-navigation/native') +}) + +jest.mock('@hyperledger/anoncreds-react-native', () => ({})) +jest.mock('@hyperledger/aries-askar-react-native', () => ({})) +jest.mock('@hyperledger/indy-vdr-react-native', () => ({})) +jest.useFakeTimers({ legacyFakeTimers: true }) +jest.spyOn(global, 'setTimeout') + +describe('displays a credential selection screen', () => { + const testEmail = 'test@email.com' + const testTime = '2022-02-11 20:00:18.180718' + const testAge = '16' + const testEmail2 = 'test2@email.com' + const testTime2 = '2023-02-11 20:00:18.180718' + const testAge2 = '17' + + const { id: presentationMessageId } = new V1RequestPresentationMessage({ + comment: 'some comment', + requestAttachments: [ + new Attachment({ + id: INDY_PROOF_REQUEST_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + json: { + name: 'test proof request', + version: '1.0.0', + nonce: '1', + requestedAttributes: { + email: { + name: 'email', + }, + time: { + name: 'time', + }, + }, + requestedPredicates: { + age: { + name: 'age', + pType: '<=', + pValue: 18, + }, + }, + }, + }), + }), + ], + }) + + const attributeBase = { + referent: '', + schemaId: '', + credentialDefinitionId: 'AAAAAAAAAAAAAAAAAAAAAA:1:AA:1234:test', + toJSON: jest.fn(), + } + + const testProofRequest = new ProofExchangeRecord({ + connectionId: '', + threadId: presentationMessageId, + state: ProofState.RequestReceived, + protocolVersion: 'V1', + }) + + const testProofFormatData = { + request: { + indy: { + requested_attributes: { + email: { + name: 'email', + restrictions: [ + { + cred_def_id: attributeBase.credentialDefinitionId, + }, + ], + }, + time: { + name: 'time', + restrictions: [ + { + cred_def_id: attributeBase.credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '<=', + p_value: 18, + restrictions: [{ cred_def_id: attributeBase.credentialDefinitionId }], + }, + }, + }, + }, + } + + const { id: credentialId } = new CredentialExchangeRecord({ + threadId: '1', + state: CredentialState.Done, + credentialAttributes: [ + { + name: 'email', + value: testEmail, + toJSON: jest.fn(), + }, + { + name: 'time', + value: testTime, + toJSON: jest.fn(), + }, + { + name: 'age', + value: testAge, + toJSON: jest.fn(), + }, + ], + protocolVersion: 'v1', + }) + const { id: credentialId2 } = new CredentialExchangeRecord({ + threadId: '1', + state: CredentialState.Done, + credentialAttributes: [ + { + name: 'email', + value: testEmail2, + toJSON: jest.fn(), + }, + { + name: 'time', + value: testTime2, + toJSON: jest.fn(), + }, + { + name: 'age', + value: testAge2, + toJSON: jest.fn(), + }, + ], + protocolVersion: 'v1', + }) + + const testRetrievedCredentials2 = { + proofFormats: { + indy: { + predicates: { + age: [ + { + credentialId: credentialId, + revealed: true, + credentialInfo: { + ...attributeBase, + credentialId: credentialId, + attributes: { age: testAge }, + }, + }, + { + credentialId: credentialId2, + revealed: true, + credentialInfo: { + ...attributeBase, + credentialId: credentialId2, + attributes: { age: testAge2 }, + }, + }, + ], + }, + attributes: { + email: [ + { + credentialId: credentialId, + revealed: true, + credentialInfo: { + ...attributeBase, + credentialId: credentialId, + attributes: { email: testEmail }, + }, + }, + { + credentialId: credentialId2, + revealed: true, + credentialInfo: { + ...attributeBase, + credentialId: credentialId2, + attributes: { email: testEmail2 }, + }, + }, + ], + time: [ + { + credentialId: credentialId, + revealed: true, + credentialInfo: { + ...attributeBase, + attributes: { time: testTime }, + credentialId: credentialId, + }, + }, + { + credentialId: credentialId2, + revealed: true, + credentialInfo: { + ...attributeBase, + attributes: { time: testTime2 }, + credentialId: credentialId2, + }, + }, + ], + }, + }, + }, + } + afterEach(() => { + cleanup() + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('with multiple credentials', () => { + + beforeEach(() => { + jest.clearAllMocks() + + // @ts-ignore-next-line + useProofById.mockReturnValue(testProofRequest) + }) + + test('test credential selection', async () => { + const { agent } = useAgent() + + // @ts-ignore-next-line + agent?.proofs.getFormatData.mockResolvedValue(testProofFormatData) + + // @ts-ignore-next-line + agent?.proofs.getCredentialsForRequest.mockResolvedValue(testRetrievedCredentials2) + + const navigation = useNavigation() + + const onCredChange = jest.fn() + const tree = render( + + + + + + ) + + await waitFor(() => { + timeTravel(1000) + }) + + const firstCred = tree.getByTestId(testIdWithKey(`select:${credentialId}`)) + expect(firstCred).not.toBeNull() + fireEvent(firstCred, 'press') + expect(navigation.goBack).toBeCalledTimes(1) + expect(onCredChange).toBeCalledTimes(1) + }) + }) +}) \ No newline at end of file diff --git a/packages/legacy/core/__tests__/screens/ProofRequest.test.tsx b/packages/legacy/core/__tests__/screens/ProofRequest.test.tsx index 3305ff032a..df6d93d179 100644 --- a/packages/legacy/core/__tests__/screens/ProofRequest.test.tsx +++ b/packages/legacy/core/__tests__/screens/ProofRequest.test.tsx @@ -5,7 +5,7 @@ import { useAgent, useProofById } from '@aries-framework/react-hooks' import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock' import { useNavigation } from '@react-navigation/core' import '@testing-library/jest-native/extend-expect' -import { cleanup, render, waitFor } from '@testing-library/react-native' +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react-native' import React from 'react' import { ConfigurationContext } from '../../App/contexts/configuration' @@ -149,7 +149,7 @@ describe('displays a proof request screen', () => { }, }, requested_predicates: { - additionalProp2: { + age: { name: 'age', p_type: '<=', p_value: 18, @@ -283,6 +283,159 @@ describe('displays a proof request screen', () => { expect(declineButton).not.toBeNull() }) + test('displays a proof request with multiple satisfying credentials', async () => { + const { agent } = useAgent() + const testEmail2 = 'test2@email.com' + const testTime2 = '2023-02-11 20:00:18.180718' + const testAge2 = '17' + + const { id: credentialId2 } = new CredentialExchangeRecord({ + threadId: '1', + state: CredentialState.Done, + credentialAttributes: [ + { + name: 'email', + value: testEmail2, + toJSON: jest.fn(), + }, + { + name: 'time', + value: testTime2, + toJSON: jest.fn(), + }, + { + name: 'age', + value: testAge2, + toJSON: jest.fn(), + }, + ], + protocolVersion: 'v1', + }) + + const testRetrievedCredentials2 = { + proofFormats: { + indy: { + predicates: { + age: [ + { + credentialId: credentialId, + revealed: true, + credentialInfo: { + ...attributeBase, + credentialId: credentialId, + attributes: { age: testAge }, + }, + }, + { + credentialId: credentialId2, + revealed: true, + credentialInfo: { + ...attributeBase, + credentialId: credentialId2, + attributes: { age: testAge2 }, + }, + }, + ], + }, + attributes: { + email: [ + { + credentialId: credentialId, + revealed: true, + credentialInfo: { + ...attributeBase, + credentialId: credentialId, + attributes: { email: testEmail }, + }, + }, + { + credentialId: credentialId2, + revealed: true, + credentialInfo: { + ...attributeBase, + credentialId: credentialId2, + attributes: { email: testEmail2 }, + }, + }, + ], + time: [ + { + credentialId: credentialId, + revealed: true, + credentialInfo: { + ...attributeBase, + attributes: { time: testTime }, + credentialId: credentialId, + }, + }, + { + credentialId: credentialId2, + revealed: true, + credentialInfo: { + ...attributeBase, + attributes: { time: testTime2 }, + credentialId: credentialId2, + }, + }, + ], + }, + }, + }, + } + + // @ts-ignore-next-line + agent?.proofs.getFormatData.mockResolvedValue(testProofFormatData) + + // @ts-ignore-next-line + agent?.proofs.getCredentialsForRequest.mockResolvedValue(testRetrievedCredentials2) + + const navigation = useNavigation() + + const { getByText, getByTestId, queryByText } = render( + + + + + + ) + + await waitFor(() => { + Promise.resolve() + }) + const changeCred = getByText('ProofRequest.ChangeCredential', { exact: false }) + const changeCredButton = getByTestId(testIdWithKey('changeCredential')) + const contact = getByText('ContactDetails.AContact', { exact: false }) + const missingInfo = queryByText('ProofRequest.IsRequestingSomethingYouDontHaveAvailable', { exact: false }) + const missingClaim = queryByText('ProofRequest.NotAvailableInYourWallet', { exact: false }) + const emailLabel = getByText(/Email/, { exact: false }) + const emailValue = getByText(testEmail) + const timeLabel = getByText(/Time/, { exact: false }) + const timeValue = getByText(testTime) + const shareButton = getByTestId(testIdWithKey('Share')) + const declineButton = getByTestId(testIdWithKey('Decline')) + + expect(changeCred).not.toBeNull() + expect(changeCredButton).not.toBeNull() + expect(contact).not.toBeNull() + expect(contact).toBeTruthy() + expect(missingInfo).toBeNull() + expect(emailLabel).not.toBeNull() + expect(emailLabel).toBeTruthy() + expect(emailValue).not.toBeNull() + expect(emailValue).toBeTruthy() + expect(timeLabel).not.toBeNull() + expect(timeLabel).toBeTruthy() + expect(timeValue).not.toBeNull() + expect(timeValue).toBeTruthy() + expect(missingClaim).toBeNull() + expect(shareButton).not.toBeNull() + expect(shareButton).toBeEnabled() + expect(declineButton).not.toBeNull() + + fireEvent(changeCredButton, 'press') + expect(navigation.navigate).toBeCalledTimes(1) + }) + test('displays a proof request with one or more claims not available', async () => { const { agent } = useAgent() diff --git a/packages/legacy/core/__tests__/screens/__snapshots__/CredentialOffer.test.tsx.snap b/packages/legacy/core/__tests__/screens/__snapshots__/CredentialOffer.test.tsx.snap index 5130d49c6e..c4b0fc18d9 100644 --- a/packages/legacy/core/__tests__/screens/__snapshots__/CredentialOffer.test.tsx.snap +++ b/packages/legacy/core/__tests__/screens/__snapshots__/CredentialOffer.test.tsx.snap @@ -310,6 +310,7 @@ exports[`displays a credential offer screen accepting a credential 1`] = ` "elevation": 0, "overflow": "hidden", }, + undefined, ] } > @@ -500,6 +501,7 @@ exports[`displays a credential offer screen accepting a credential 1`] = ` @@ -1846,6 +1855,7 @@ exports[`displays a credential offer screen declining a credential 1`] = ` "elevation": 0, "overflow": "hidden", }, + undefined, ] } > @@ -2036,6 +2046,7 @@ exports[`displays a credential offer screen declining a credential 1`] = ` @@ -3602,6 +3620,7 @@ exports[`displays a credential offer screen renders correctly 1`] = ` "elevation": 0, "overflow": "hidden", }, + undefined, ] } > @@ -3792,6 +3811,7 @@ exports[`displays a credential offer screen renders correctly 1`] = ` diff --git a/packages/oca/src/legacy/resolver/record.ts b/packages/oca/src/legacy/resolver/record.ts index 17c67943c2..39a7a7b93c 100644 --- a/packages/oca/src/legacy/resolver/record.ts +++ b/packages/oca/src/legacy/resolver/record.ts @@ -1,5 +1,4 @@ import { AnonCredsNonRevokedInterval, AnonCredsProofRequestRestriction } from '@aries-framework/anoncreds' -import { CredentialExchangeRecord } from '@aries-framework/core' export interface FieldParams { name: string | null @@ -77,21 +76,3 @@ export class Predicate extends Field { this.satisfied = params.satisfied } } - -export interface ProofCredentialAttributes { - credExchangeRecord?: CredentialExchangeRecord - credDefId?: string - schemaId?: string - credName: string - attributes?: Attribute[] -} - -export interface ProofCredentialPredicates { - credExchangeRecord?: CredentialExchangeRecord - credDefId?: string - schemaId?: string - credName: string - predicates?: Predicate[] -} - -export interface ProofCredentialItems extends ProofCredentialAttributes, ProofCredentialPredicates {}