Skip to content

Commit 19dcd71

Browse files
committed
feat(dc-api): use aptitude matcher for openid4vp
1 parent 2fe85d4 commit 19dcd71

12 files changed

Lines changed: 241 additions & 125 deletions

apps/easypid/metro.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default {
1313
...config.resolver,
1414

1515
nodeModulesPaths: [path.resolve(projectRoot, 'node_modules'), path.resolve(workspaceRoot, 'node_modules')],
16+
assetExts: [...(config.resolver?.assetExts ?? []), 'wasm'],
1617
sourceExts: [...(config.resolver?.sourceExts ?? []), 'js', 'json', 'ts', 'tsx', 'cjs', 'mjs'],
1718
extraNodeModules: {
1819
// Needed for cosmjs trying to import node crypto

apps/easypid/src/app/(app)/_layout.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { TypedArrayEncoder } from '@credo-ts/core'
22
import { useHasFinishedOnboarding } from '@easypid/features/onboarding'
33
import { useFeatureFlag } from '@easypid/hooks/useFeatureFlag'
44
import { useResetWalletDevMenu } from '@easypid/hooks/useResetWalletDevMenu'
5+
import { dcApiRegisterOptions } from '@easypid/utils/dcApiRegisterOptions'
56
import { type CredentialDataHandlerOptions, useHaptics } from '@package/app'
67
import { HeroIcons, IconContainer } from '@package/ui'
78
import type { InvitationType } from '@paradym/wallet-sdk'
89
import { activityStorage, deferredCredentialStorage, ParadymWalletSdk, useParadym } from '@paradym/wallet-sdk'
910
import { Redirect, Stack, useGlobalSearchParams, usePathname, useRouter } from 'expo-router'
10-
import { useState } from 'react'
11+
import { useEffect, useRef, useState } from 'react'
1112
import { Pressable } from 'react-native-gesture-handler'
1213
import { useTheme } from 'tamagui'
1314

@@ -34,9 +35,17 @@ export default function AppLayout() {
3435
const router = useRouter()
3536
const { withHaptics } = useHaptics()
3637
const [redirectAfterUnlocked, setRedirectAfterUnlocked] = useState<string>()
38+
const registeredDcApiWalletId = useRef<string | undefined>(undefined)
3739
const pathname = usePathname()
3840
const params = useGlobalSearchParams()
3941

42+
useEffect(() => {
43+
if (paradym.state !== 'unlocked' || registeredDcApiWalletId.current === paradym.paradym.walletId) return
44+
45+
registeredDcApiWalletId.current = paradym.paradym.walletId
46+
void paradym.paradym.dcApi.registerCredentials(dcApiRegisterOptions({ paradym: paradym.paradym }))
47+
}, [paradym])
48+
4049
// It could be that the onboarding is cut of mid-process, and e.g. the user closes the app
4150
// if this is the case we will redo the onboarding
4251
const [hasFinishedOnboarding] = useHasFinishedOnboarding()

apps/easypid/src/features/share/DcApiSharingScreen.tsx

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { paradymWalletSdkOptions } from '@easypid/config/paradym'
12
import { setupWalletServiceProvider } from '@easypid/crypto/WalletServiceProviderClient'
23
import { useLingui } from '@lingui/react/macro'
34
import { PinDotsInput, type PinDotsInputRef } from '@package/app'
45
import { commonMessages, TranslationProvider } from '@package/translations'
56
import { Heading, Paragraph, Stack, TamaguiProvider, YStack } from '@package/ui'
67
import type { DigitalCredentialsRequest } from '@paradym/wallet-sdk'
7-
import { useParadym } from '@paradym/wallet-sdk'
8-
import { useRef, useState } from 'react'
8+
import { ParadymWalletAuthenticationInvalidPinError, ParadymWalletSdk, useParadym } from '@paradym/wallet-sdk'
9+
import { useEffect, useRef, useState } from 'react'
910
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
1011
import tamaguiConfig from '../../../tamagui.config'
1112
import { useStoredLocale } from '../../hooks/useStoredLocale'
@@ -18,15 +19,17 @@ export function DcApiSharingScreen({ request }: DcApiSharingScreenProps) {
1819
const [storedLocale] = useStoredLocale()
1920

2021
return (
21-
<TranslationProvider customLocale={storedLocale}>
22-
<TamaguiProvider disableInjectCSS defaultTheme="light" config={tamaguiConfig}>
23-
<SafeAreaProvider>
24-
<Stack flex-1 justifyContent="flex-end">
25-
<DcApiSharingScreenWithContext request={request} />
26-
</Stack>
27-
</SafeAreaProvider>
28-
</TamaguiProvider>
29-
</TranslationProvider>
22+
<ParadymWalletSdk.UnlockProvider configuration={paradymWalletSdkOptions}>
23+
<TranslationProvider customLocale={storedLocale}>
24+
<TamaguiProvider disableInjectCSS defaultTheme="light" config={tamaguiConfig}>
25+
<SafeAreaProvider>
26+
<Stack flex-1 justifyContent="flex-end">
27+
<DcApiSharingScreenWithContext request={request} />
28+
</Stack>
29+
</SafeAreaProvider>
30+
</TamaguiProvider>
31+
</TranslationProvider>
32+
</ParadymWalletSdk.UnlockProvider>
3033
)
3134
}
3235

@@ -37,63 +40,65 @@ export function DcApiSharingScreenWithContext({ request }: DcApiSharingScreenPro
3740
const { t } = useLingui()
3841
const paradym = useParadym()
3942

40-
const onShareResponse = async () => {
41-
if (paradym.state !== 'unlocked') {
42-
throw new Error(`Invalid state for paradym wallet sdk. Expected 'unlocked', received '${paradym.state}'`)
43-
}
44-
45-
const resolvedRequest = await paradym.paradym.dcApi
43+
const onShareResponse = async (sdk: ParadymWalletSdk) => {
44+
const resolvedRequest = await sdk.dcApi
4645
.resolveRequest({ request })
47-
.then((resolvedRequest) => {
48-
// We can't share multiple documents at the moment
49-
if (resolvedRequest.formattedSubmission.entries.length > 1) {
50-
throw new Error('Multiple cards requested, but only one card can be shared with the digital credentials api.')
51-
}
52-
53-
return resolvedRequest
54-
})
46+
.then((resolvedRequest) => resolvedRequest)
5547
.catch((error) => {
56-
paradym.paradym.logger.error('Error getting credentials for dc api request', {
48+
sdk.logger.error('Error getting credentials for dc api request', {
5749
error,
5850
})
5951

6052
// Not shown to the user
61-
paradym.paradym.dcApi.sendErrorResponse('Presentation information could not be extracted')
53+
sdk.dcApi.sendErrorResponse('Presentation information could not be extracted')
6254
})
6355

6456
if (!resolvedRequest) return
6557

6658
// Once this returns we just assume it's successful
6759
try {
68-
await paradym.paradym.dcApi.sendResponse({
60+
await sdk.dcApi.sendResponse({
6961
dcRequest: request,
7062
resolvedRequest,
7163
})
7264
} catch (error) {
73-
paradym.paradym.logger.error('Could not share response', { error })
65+
sdk.logger.error('Could not share response', { error })
7466

7567
// Not shown to the user
76-
paradym.paradym.dcApi.sendErrorResponse('Unable to share credentials')
68+
sdk.dcApi.sendErrorResponse('Unable to share credentials')
7769
return
7870
}
7971
}
8072

73+
useEffect(() => {
74+
if (!isProcessing || paradym.state !== 'acquired-wallet-key') return
75+
76+
paradym
77+
.unlock()
78+
.then(async (sdk) => {
79+
await setupWalletServiceProvider(sdk)
80+
await onShareResponse(sdk)
81+
})
82+
.catch((error) => {
83+
if (error instanceof ParadymWalletAuthenticationInvalidPinError) {
84+
pinRef.current?.clear()
85+
pinRef.current?.shake()
86+
}
87+
})
88+
.finally(() => setIsProcessing(false))
89+
}, [paradym, isProcessing])
90+
8191
const onUnlockSdk = async (pin: string) => {
92+
if (paradym.state !== 'locked') return
93+
8294
setIsProcessing(true)
83-
if (paradym.state === 'locked') {
95+
try {
8496
await paradym.unlockUsingPin(pin)
97+
} catch (_error) {
98+
pinRef.current?.clear()
99+
pinRef.current?.shake()
100+
setIsProcessing(false)
85101
}
86-
87-
if (paradym.state === 'acquired-wallet-key') {
88-
const sdk = await paradym.unlock()
89-
await setupWalletServiceProvider(sdk)
90-
}
91-
92-
if (paradym.state === 'unlocked') {
93-
await onShareResponse()
94-
}
95-
96-
throw new Error(`Invalid state. Received: '${paradym.state}'`)
97102
}
98103

99104
return (
@@ -113,7 +118,7 @@ export function DcApiSharingScreenWithContext({ request }: DcApiSharingScreenPro
113118
<Stack pt="$5">
114119
<PinDotsInput
115120
onPinComplete={onUnlockSdk}
116-
isLoading={isProcessing}
121+
isLoading={isProcessing || paradym.state === 'initializing'}
117122
pinLength={6}
118123
ref={pinRef}
119124
useNativeKeyboard={false}
Binary file not shown.
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
},
2525
"pnpm": {
2626
"overrides": {
27+
"@animo-id/expo-digital-credentials-api": "file:dep_packages/animo-id-expo-digital-credentials-api-0.0.0-local-20260223190806.tgz",
2728
"react-native-fs": "catalog:"
2829
},
2930
"patchedDependencies": {

packages/sdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"dependencies": {
2727
"@animo-id/eudi-wallet-functionality": "catalog:",
2828
"@animo-id/expo-digital-credentials-api": "catalog:",
29+
"@animo-id/expo-digital-credentials-api-aptitude-consortium": "file:../../dep_packages/animo-id-expo-digital-credentials-api-aptitude-consortium-0.0.0-20260430150021.tgz",
2930
"@astronautlabs/jsonpath": "catalog:",
3031
"@credo-ts/anoncreds": "catalog:",
3132
"@credo-ts/askar": "catalog:",

packages/sdk/src/dcApi/registerCredentials.ts

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { type RegisterCredentialsOptions, registerCredentials } from '@animo-id/expo-digital-credentials-api'
2-
import { DateOnly, type Logger, type MdocNameSpaces, type MdocRecord } from '@credo-ts/core'
1+
import {
2+
type AptitudeConsortiumConfig,
3+
registerCredentials,
4+
} from '@animo-id/expo-digital-credentials-api-aptitude-consortium'
5+
import { DateOnly, type Logger, type MdocNameSpaces, type MdocRecord, type SdJwtVcRecord } from '@credo-ts/core'
36
import { t } from '@lingui/core/macro'
47
import { commonMessages, i18n } from '@package/translations'
58
import { ImageFormat, Skia } from '@shopify/react-native-skia'
@@ -12,8 +15,9 @@ import { getCredentialForDisplay } from '../display/credential'
1215
import { resolveClaimsWithRecordMetadata, resolveLabelFromClaimsPath } from '../format/attributes'
1316
import type { ParadymWalletSdk } from '../ParadymWalletSdk'
1417

15-
type CredentialItem = RegisterCredentialsOptions['credentials'][number]
16-
type CredentialDisplayClaim = NonNullable<CredentialItem['display']['claims']>[number]
18+
type CredentialItem = NonNullable<AptitudeConsortiumConfig['credentials']>[number]
19+
type CredentialDisplayClaim = NonNullable<CredentialItem['fields']>[number]
20+
const noTransactionDataTypes: NonNullable<CredentialItem['transaction_data_types']> = []
1721

1822
function mapMdocAttributes(namespaces: MdocNameSpaces) {
1923
return Object.fromEntries(
@@ -43,7 +47,7 @@ function mapMdocAttributesToClaimDisplay(namespaces: MdocNameSpaces, record: Mdo
4347
return Object.entries(namespaces).flatMap(([namespace, values]) =>
4448
Object.keys(values).map((key) => ({
4549
path: [namespace, key],
46-
displayName: resolveLabelFromClaimsPath([namespace, key], claims, i18n.locale) ?? t(commonMessages.unknown),
50+
display_name: resolveLabelFromClaimsPath([namespace, key], claims, i18n.locale) ?? t(commonMessages.unknown),
4751
}))
4852
)
4953
}
@@ -62,13 +66,32 @@ function mapSdJwtAttributesToClaimDisplay(
6266
return [
6367
{
6468
path: [...path, claimName],
65-
displayName: resolveLabelFromClaimsPath([...path, claimName], claims, i18n.locale) ?? t(commonMessages.unknown),
69+
display_name:
70+
resolveLabelFromClaimsPath([...path, claimName], claims, i18n.locale) ?? t(commonMessages.unknown),
6671
},
6772
...nestedClaims,
6873
]
6974
})
7075
}
7176

77+
function normalizeAptitudeIcon(iconDataUrl?: string) {
78+
if (!iconDataUrl) return undefined
79+
80+
const commaIndex = iconDataUrl.indexOf(',')
81+
return commaIndex >= 0 ? iconDataUrl.slice(commaIndex + 1) : iconDataUrl
82+
}
83+
84+
function getSdJwtVcts(record: SdJwtVcRecord) {
85+
const chainVcts = record.typeMetadataChain
86+
?.map((entry) => entry.vct)
87+
.filter((vct): vct is string => typeof vct === 'string' && vct.length > 0)
88+
89+
const tagVct = record.getTags().vct
90+
const vcts = chainVcts && chainVcts.length > 0 ? chainVcts : tagVct ? [tagVct] : []
91+
92+
return vcts.length > 0 ? Array.from(new Set(vcts)) : undefined
93+
}
94+
7295
/**
7396
* Returns base64 data url
7497
*/
@@ -196,18 +219,15 @@ export async function dcApiRegisterCredentials({
196219
: undefined
197220

198221
return {
199-
id: record.id,
200-
credential: {
201-
doctype: mdoc.docType,
202-
format: 'mso_mdoc',
203-
namespaces: mapMdocAttributes(mdoc.issuerSignedNamespaces),
204-
},
205-
display: {
206-
title: display.name ?? displayTitleFallback,
207-
subtitle: display.issuer.name ? displaySubtitle(display.issuer.name) : displaySubtitleFallback,
208-
claims: mapMdocAttributesToClaimDisplay(mdoc.issuerSignedNamespaces, record),
209-
iconDataUrl,
210-
},
222+
id: getCredentialForDisplay(record).id,
223+
format: 'mso_mdoc',
224+
title: display.name ?? displayTitleFallback,
225+
subtitle: display.issuer.name ? displaySubtitle(display.issuer.name) : displaySubtitleFallback,
226+
fields: mapMdocAttributesToClaimDisplay(mdoc.issuerSignedNamespaces, record),
227+
icon: normalizeAptitudeIcon(iconDataUrl),
228+
doctype: mdoc.docType,
229+
transaction_data_types: noTransactionDataTypes,
230+
claims: mapMdocAttributes(mdoc.issuerSignedNamespaces),
211231
} as const
212232
})
213233

@@ -224,28 +244,31 @@ export async function dcApiRegisterCredentials({
224244
const claims = resolveClaimsWithRecordMetadata(record)
225245

226246
return {
227-
id: record.id,
228-
credential: {
229-
vct: record.getTags().vct,
230-
format: 'dc+sd-jwt',
231-
// biome-ignore lint/suspicious/noExplicitAny: no explanation
232-
claims: sdJwtVc.prettyClaims as any,
233-
},
234-
display: {
235-
title: display.name ?? displayTitleFallback,
236-
subtitle: display.issuer.name ? displaySubtitle(display.issuer.name) : displaySubtitleFallback,
237-
claims: mapSdJwtAttributesToClaimDisplay(claims, record),
238-
iconDataUrl,
239-
},
247+
id: getCredentialForDisplay(record).id,
248+
format: 'dc+sd-jwt',
249+
title: display.name ?? displayTitleFallback,
250+
subtitle: display.issuer.name ? displaySubtitle(display.issuer.name) : displaySubtitleFallback,
251+
fields: mapSdJwtAttributesToClaimDisplay(claims, sdJwtVc.prettyClaims),
252+
icon: normalizeAptitudeIcon(iconDataUrl),
253+
vcts: getSdJwtVcts(record),
254+
transaction_data_types: noTransactionDataTypes,
255+
// biome-ignore lint/suspicious/noExplicitAny: no explanation
256+
claims: sdJwtVc.prettyClaims as any,
240257
} as const
241258
})
242259

243260
const credentials = await Promise.all([...sdJwtCredentials, ...mdocCredentials])
244261
paradym.logger.trace('Registering credentials for Digital Credentials API')
245262

246263
await registerCredentials({
247-
credentials,
248-
matcher: 'ubique',
264+
aptitudeConsortiumConfig: {
265+
log_level: __DEV__ ? 'debug' : undefined,
266+
dcql: {
267+
credential_set_option_mode: 'all_satisfiable',
268+
optional_credential_sets_mode: 'prefer_present',
269+
},
270+
credentials,
271+
},
249272
})
250273
} catch (error) {
251274
// Since this is an experimental feature, and it doedisplayTitleFallbacksn't work if you don't have the latest

0 commit comments

Comments
 (0)