Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/select-credentia…
Browse files Browse the repository at this point in the history
…ls-now
  • Loading branch information
TimoGlastra committed Jul 13, 2024
2 parents 35b6e34 + e62a97a commit bd03aee
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 93 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ With Paradym Wallet, you can seamlessly manage and present your digital credenti

You can download Paradym Wallet from the [Google Play Store](https://play.google.com/store/apps/details?id=id.paradym.wallet) or [Apple App Store](https://apps.apple.com/nl/app/paradym-wallet/id6449846111?l=en).

The wallet can be used in three environments:
The wallet can be used with existing demo's in these environments:

- Dutch Blockchain Coalition (DBC): Use your wallet to access the DBC zone where you can find extra resources related to DBC events. You can use [this link](https://ssi.dutchblockchaincoalition.org/demo/issuer) to receive your credential, and log in on the [DBC website](https://www.dutchblockchaincoalition.org/userlogin).
- Triall: Log in to the Triall environment. Obtain your credential [here](https://ssi.triall.io/demo/issuer) and enter the environment on the [Trial website](https://ssi.triall.io/demo/issuer).
- Future Mobility Alliance: Access the Future Mobility Data Marketplace by obtaining your credential [here](https://ssi.future-mobility-alliance.org/demo/issuer) and logging in via the [FMA website](https://marketplace.future-mobility-alliance.org/).

## Project Structure
Expand Down
202 changes: 119 additions & 83 deletions packages/agent/src/hooks/useDidCommPresentationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
AnonCredsSelectedCredentials,
} from '@credo-ts/anoncreds'
import type { ProofStateChangedEvent } from '@credo-ts/core'
import type { FormattedSubmission } from '../format/formatPresentation'
import type { FormattedSubmission, FormattedSubmissionEntry } from '../format/formatPresentation'

import { CredentialRepository, CredoError, ProofEventTypes, ProofState } from '@credo-ts/core'
import { useConnectionById, useProofById } from '@credo-ts/react-hooks'
Expand Down Expand Up @@ -42,10 +42,64 @@ export function useDidCommPresentationActions(proofExchangeId: string) {
throw new CredoError('Invalid proof request.')
}

const submission: FormattedSubmission = {
areAllSatisfied: false,
entries: [],
name: proofRequest?.name ?? 'Unknown',
const entries = new Map<
string,
{
groupNames: {
attributes: string[]
predicates: string[]
}
matches: Array<AnonCredsRequestedPredicateMatch>
requestedAttributes: Set<string>
}
>()

const mergeOrSetEntry = (
type: 'attribute' | 'predicate',
groupName: string,
requestedAttributeNames: string[],
matches: AnonCredsRequestedAttributeMatch[] | AnonCredsRequestedPredicateMatch[]
) => {
// We create an entry hash. This way we can group all items that have the same credentials
// available. If no credentials are available for a group, we create a entry hash based
// on the group name
const entryHash = groupName.includes('__CREDENTIAL__')
? groupName.split('__CREDENTIAL__')[0]
: matches.length > 0
? matches
.map((a) => a.credentialId)
.sort()
.join(',')
: groupName

const entry = entries.get(entryHash)

if (!entry) {
entries.set(entryHash, {
groupNames: {
attributes: type === 'attribute' ? [groupName] : [],
predicates: type === 'predicate' ? [groupName] : [],
},
matches,
requestedAttributes: new Set(requestedAttributeNames),
})
return
}

if (type === 'attribute') {
entry.groupNames.attributes.push(groupName)
} else {
entry.groupNames.predicates.push(groupName)
}

entry.requestedAttributes = new Set([...requestedAttributeNames, ...entry.requestedAttributes])

// We only include the matches which are present in both entries. If we use the __CREDENTIAL__ it means we can only use
// credentials that match both (we want this in Paradym). For the other ones we create a 'hash' from all available credentialIds
// first already, so it should give the same result.
entry.matches = entry.matches.filter((match) =>
matches.some((innerMatch) => match.credentialId === innerMatch.credentialId)
)
}

const allCredentialIds = [
Expand All @@ -60,75 +114,52 @@ export function useDidCommPresentationActions(proofExchangeId: string) {
$or: allCredentialIds.map((credentialId) => ({ credentialIds: [credentialId] })),
})

const attributes = anonCredsCredentials.attributes
await Promise.all(
Object.keys(attributes).map(async (groupName) => {
const requestedAttribute = proofRequest.requested_attributes[groupName]
const attributeNames = requestedAttribute?.names ?? [requestedAttribute?.name as string]
const attributeArray = attributes[groupName] as AnonCredsRequestedAttributeMatch[]

submission.entries.push({
inputDescriptorId: groupName,
isSatisfied: attributeArray.length >= 1,
name: groupName, // TODO
credentials: attributeArray.map((attribute) => {
const credentialExchange = credentialExchanges.find((c) =>
c.credentials.find((cc) => cc.credentialRecordId === attribute.credentialId)
)
const credentialDisplayMetadata = credentialExchange
? getDidCommCredentialExchangeDisplayMetadata(credentialExchange)
: undefined

return {
name: groupName, // TODO: humanize string? Or should we let this out?
credentialName: credentialDisplayMetadata?.credentialName ?? 'Credential',
isSatisfied: true,
issuerName: credentialDisplayMetadata?.issuerName ?? 'Unknown',
requestedAttributes: attributeNames,
}
}),
})
})
)
for (const [groupName, attributeArray] of Object.entries(anonCredsCredentials.attributes)) {
const requestedAttribute = proofRequest.requested_attributes[groupName]
if (!requestedAttribute) throw new Error('Invalid presentation request')
const requestedAttributesNames = requestedAttribute.names ?? [requestedAttribute.name as string]

const predicates = anonCredsCredentials.predicates
await Promise.all(
Object.keys(predicates).map(async (groupName) => {
const requestedPredicate = proofRequest.requested_predicates[groupName]
const predicateArray = predicates[groupName] as AnonCredsRequestedPredicateMatch[]
mergeOrSetEntry('attribute', groupName, requestedAttributesNames, attributeArray)
}

if (!requestedPredicate) {
throw new Error('Invalid presentation request')
}
for (const [groupName, predicateArray] of Object.entries(anonCredsCredentials.predicates)) {
const requestedPredicate = proofRequest.requested_predicates[groupName]
if (!requestedPredicate) throw new Error('Invalid presentation request')

submission.entries.push({
inputDescriptorId: groupName,
isSatisfied: predicateArray.length >= 1,
name: groupName, // TODO
credentials: predicateArray.map((predicate) => {
const credentialExchange = credentialExchanges.find((c) =>
c.credentials.find((cc) => cc.credentialRecordId === predicate.credentialId)
)
const credentialDisplayMetadata = credentialExchange
? getDidCommCredentialExchangeDisplayMetadata(credentialExchange)
: undefined

return {
name: groupName, // TODO: humanize string? Or should we let this out?
credentialName: credentialDisplayMetadata?.credentialName ?? 'Credential',
isSatisfied: true,
issuerName: credentialDisplayMetadata?.issuerName ?? 'Unknown',
// TODO: we need to group multiple predicates/attributes for the same credential into one.
requestedAttributes: [formatPredicate(requestedPredicate)],
}
}),
})
})
)
mergeOrSetEntry('predicate', groupName, [formatPredicate(requestedPredicate)], predicateArray)
}

submission.areAllSatisfied = submission.entries.every((entry) => entry.isSatisfied)
const entriesArray = Array.from(entries.entries()).map(([entryHash, entry]): FormattedSubmissionEntry => {
return {
inputDescriptorId: entryHash,
credentials: entry.matches.map((match) => {
const credentialExchange = credentialExchanges.find((c) =>
c.credentials.find((cc) => cc.credentialRecordId === match.credentialId)
)
const credentialDisplayMetadata = credentialExchange
? getDidCommCredentialExchangeDisplayMetadata(credentialExchange)
: undefined

return {
credentialName: credentialDisplayMetadata?.credentialName ?? 'Credential',
isSatisfied: true,
issuerName: credentialDisplayMetadata?.issuerName ?? 'Unknown',
requestedAttributes: Array.from(entry.requestedAttributes),
}
}),
isSatisfied: entry.matches.length > 0,
// TODO: we can fetch the schema name based on requirements
name: 'Credential',
}
})

const submission: FormattedSubmission = {
areAllSatisfied: entriesArray.every((entry) => entry.isSatisfied),
entries: entriesArray,
name: proofRequest?.name ?? 'Unknown',
}

return { submission, formatKey, anonCredsCredentials }
return { submission, formatKey, entries }
},
})

Expand All @@ -139,21 +170,26 @@ export function useDidCommPresentationActions(proofExchangeId: string) {
undefined

if (selectedCredentials && Object.keys(selectedCredentials).length > 0) {
if (!data?.formatKey || !data.anonCredsCredentials)
throw new Error('Unable to accept presentation without credentials')
const selectedAttributes = Object.fromEntries(
Object.entries(data.anonCredsCredentials.attributes).map(([groupName, matches]) => [
groupName,
matches[selectedCredentials[groupName] ?? 0],
])
)
if (!data?.formatKey || !data.entries) throw new Error('Unable to accept presentation without credentials')

const selectedPredicates = Object.fromEntries(
Object.entries(data.anonCredsCredentials.predicates).map(([groupName, matches]) => [
groupName,
matches[selectedCredentials[groupName] ?? 0],
])
)
const selectedAttributes: Record<string, AnonCredsRequestedAttributeMatch> = {}
const selectedPredicates: Record<string, AnonCredsRequestedPredicateMatch> = {}

for (const [inputDescriptorId, entry] of Array.from(data.entries.entries())) {
const matchIndex = selectedCredentials[inputDescriptorId] ?? 0
const match = entry.matches[matchIndex]

for (const groupName of entry.groupNames.attributes) {
selectedAttributes[groupName] = {
...match,
revealed: true,
}
}

for (const groupName of entry.groupNames.predicates) {
selectedPredicates[groupName] = match
}
}

formatInput = {
[data.formatKey]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function DidCommNotificationScreen() {
const toast = useToastController()
const router = useRouter()

const [isLoadingInvitation, setIsLoadingInvitation] = useState(false)
const [hasHandledNotificationLoading, setHasHandledNotificationLoading] = useState(false)
const [notification, setNotification] = useState(
params.credentialExchangeId
? ({
Expand All @@ -48,7 +48,8 @@ export function DidCommNotificationScreen() {

useEffect(() => {
async function handleInvitation() {
if (isLoadingInvitation) return
if (hasHandledNotificationLoading) return
setHasHandledNotificationLoading(true)

try {
const invitation = params.invitation
Expand All @@ -57,8 +58,8 @@ export function DidCommNotificationScreen() {
? decodeURIComponent(params.invitationUrl)
: undefined

// Might be no invitation if a presentationExchangeId or credentialExchangeId is passed directly
if (!invitation) return
setIsLoadingInvitation(true)

const parseResult = await parseDidCommInvitation(agent, invitation)
if (!parseResult.success) {
Expand Down Expand Up @@ -86,12 +87,10 @@ export function DidCommNotificationScreen() {
toast.show('Error parsing invitation')
pushToWallet()
}

setIsLoadingInvitation(false)
}

void handleInvitation()
}, [params.invitation, params.invitationUrl, isLoadingInvitation, agent, toast, pushToWallet])
}, [params.invitation, params.invitationUrl, hasHandledNotificationLoading, agent, toast, pushToWallet])

// We were routed here without any notification
if (!params.credentialExchangeId && !params.proofExchangeId && !params.invitation && !params.invitationUrl) {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/hooks/useTransparentNavigationBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as NavigationBar from 'expo-navigation-bar'

import { isAndroid } from '../utils'
import { isAndroid } from '../utils/platform'

export const useTransparentNavigationBar = () => {
if (isAndroid()) {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/utils/DeeplinkHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { useCallback } from 'react'
import type { ReactNode } from 'react'

import { InvitationQrTypes } from '@package/agent'
import { useCredentialDataHandler } from '@package/app'
import { useToastController } from '@package/ui'
import { CommonActions } from '@react-navigation/native'
import * as Linking from 'expo-linking'
import { useNavigation } from 'expo-router'
import { useEffect, useState } from 'react'
import { useCredentialDataHandler } from '../hooks'

interface DeeplinkHandlerProps {
children: ReactNode
Expand Down

0 comments on commit bd03aee

Please sign in to comment.