diff --git a/apps/easypid/src/app/(app)/_layout.tsx b/apps/easypid/src/app/(app)/_layout.tsx
index d5084138..ac4fd2d5 100644
--- a/apps/easypid/src/app/(app)/_layout.tsx
+++ b/apps/easypid/src/app/(app)/_layout.tsx
@@ -10,7 +10,6 @@ import { type CredentialDataHandlerOptions, DeeplinkHandler, useHaptics } from '
import { HeroIcons, IconContainer } from '@package/ui'
import { useEffect, useState } from 'react'
import { useTheme } from 'tamagui'
-import { WithBackgroundPidRefresh } from '../../features/pid/WithBackPidRefresh'
const jsonRecordIds = [activityStorage.recordId]
@@ -99,48 +98,46 @@ export default function AppLayout() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
diff --git a/apps/easypid/src/features/pid/WithBackPidRefresh.tsx b/apps/easypid/src/features/pid/WithBackPidRefresh.tsx
deleted file mode 100644
index ff00fcaa..00000000
--- a/apps/easypid/src/features/pid/WithBackPidRefresh.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { PropsWithChildren } from 'react'
-import { useBackgroundPidRefresh } from '../../hooks/useBackgroundPidRefresh'
-
-export function WithBackgroundPidRefresh({ children }: PropsWithChildren) {
- // Refresh PID once it reaches 1
- // useBackgroundPidRefresh(1)
-
- return children
-}
diff --git a/apps/easypid/src/hooks/useBackgroundPidRefresh.ts b/apps/easypid/src/hooks/useBackgroundPidRefresh.ts
deleted file mode 100644
index 5b8725d6..00000000
--- a/apps/easypid/src/hooks/useBackgroundPidRefresh.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import type { MdocRecord, SdJwtVcRecord } from '@credo-ts/core'
-import { getBatchCredentialMetadata } from '@package/agent/src/openid4vc/batchMetadata'
-import { useHasInternetConnection } from '@package/app'
-import { useEffect, useMemo, useState } from 'react'
-import { type AppAgent, useAppAgent } from '../agent'
-import { useShouldUseCloudHsm } from '../features/onboarding/useShouldUseCloudHsm'
-import { RefreshPidUseCase } from '../use-cases/RefreshPidUseCase.ts'
-import { usePidCredential } from './usePidCredential'
-
-async function refreshPid({ agent, sdJwt, mdoc }: { agent: AppAgent; sdJwt?: SdJwtVcRecord; mdoc?: MdocRecord }) {
- console.log('refreshing PID')
- const useCase = await RefreshPidUseCase.initialize({
- agent,
- })
-
- await useCase.retrieveCredentialsUsingExistingRecords({
- sdJwt,
- mdoc,
- })
-}
-
-export function useBackgroundPidRefresh(batchThreshold: number) {
- const { sdJwt, mdoc } = usePidCredential()
- const [isRefreshing, setIsRefreshing] = useState(false)
- const hasInternet = useHasInternetConnection()
- const shouldUseCloudHsm = useShouldUseCloudHsm()
- const { agent } = useAppAgent()
-
- const { shouldRefreshMdoc, shouldRefreshSdJwt } = useMemo(() => {
- if (!shouldUseCloudHsm) return {}
-
- let shouldRefreshSdJwt = false
- if (sdJwt) {
- const sdJwtBatch = getBatchCredentialMetadata(sdJwt.record)
- if (sdJwtBatch) {
- shouldRefreshSdJwt = sdJwtBatch.additionalCredentials.length <= batchThreshold
- }
- }
-
- let shouldRefreshMdoc = false
- if (mdoc) {
- const mdocBatch = getBatchCredentialMetadata(mdoc.record)
- if (mdocBatch) {
- shouldRefreshMdoc = mdocBatch.additionalCredentials.length <= batchThreshold
- }
- }
-
- return {
- shouldRefreshSdJwt,
- shouldRefreshMdoc,
- }
- }, [sdJwt, mdoc, batchThreshold, shouldUseCloudHsm])
-
- useEffect(() => {
- if (isRefreshing || !hasInternet || !shouldUseCloudHsm) return
-
- if (shouldRefreshMdoc || shouldRefreshSdJwt) {
- setIsRefreshing(true)
-
- refreshPid({
- agent,
- sdJwt: shouldRefreshSdJwt ? sdJwt?.record : undefined,
- mdoc: shouldRefreshMdoc ? mdoc?.record : undefined,
- }).finally(() => setIsRefreshing(false))
- }
- }, [shouldRefreshMdoc, shouldRefreshSdJwt, hasInternet, agent, isRefreshing, mdoc, sdJwt, shouldUseCloudHsm])
-}
diff --git a/apps/easypid/src/use-cases/ReceivePidUseCaseFlow.ts b/apps/easypid/src/use-cases/ReceivePidUseCaseFlow.ts
index d6974bc4..4e4022b2 100644
--- a/apps/easypid/src/use-cases/ReceivePidUseCaseFlow.ts
+++ b/apps/easypid/src/use-cases/ReceivePidUseCaseFlow.ts
@@ -1,13 +1,13 @@
import { AusweisAuthFlow, type AusweisAuthFlowOptions, sendCommand } from '@animo-id/expo-ausweis-sdk'
import type { MdocRecord } from '@credo-ts/core'
import type { AppAgent } from '@easypid/agent'
-import {
- type OpenId4VciRequestTokenResponse,
- type OpenId4VciResolvedCredentialOffer,
- type OpenId4VciResolvedOauth2RedirectAuthorizationRequest,
- type SdJwtVcRecord,
- acquireAuthorizationCodeAccessToken,
+import type {
+ OpenId4VciRequestTokenResponse,
+ OpenId4VciResolvedCredentialOffer,
+ OpenId4VciResolvedOauth2RedirectAuthorizationRequest,
+ SdJwtVcRecord,
} from '@package/agent'
+import { acquireAuthorizationCodeAccessToken } from '@package/agent/src/invitation/handler'
export interface ReceivePidUseCaseFlowOptions
extends Pick {
diff --git a/apps/easypid/src/use-cases/RefreshPidUseCase.ts.ts b/apps/easypid/src/use-cases/RefreshPidUseCase.ts
similarity index 92%
rename from apps/easypid/src/use-cases/RefreshPidUseCase.ts.ts
rename to apps/easypid/src/use-cases/RefreshPidUseCase.ts
index 46f545f0..8ac91059 100644
--- a/apps/easypid/src/use-cases/RefreshPidUseCase.ts.ts
+++ b/apps/easypid/src/use-cases/RefreshPidUseCase.ts
@@ -1,17 +1,18 @@
import { ClaimFormat, MdocRecord, getJwkFromJson } from '@credo-ts/core'
+import { SdJwtVcRecord } from '@credo-ts/core'
import type { AppAgent } from '@easypid/agent'
+import type { OpenId4VciRequestTokenResponse, OpenId4VciResolvedCredentialOffer } from '@package/agent'
import {
- type OpenId4VciRequestTokenResponse,
- type OpenId4VciResolvedCredentialOffer,
- SdJwtVcRecord,
acquireRefreshTokenAccessToken,
- getRefreshCredentialMetadata,
receiveCredentialFromOpenId4VciOffer,
resolveOpenId4VciOffer,
- setRefreshCredentialMetadata,
- updateCredential,
-} from '@package/agent'
+} from '@package/agent/src/invitation/handler'
import { getBatchCredentialMetadata, setBatchCredentialMetadata } from '@package/agent/src/openid4vc/batchMetadata'
+import {
+ getRefreshCredentialMetadata,
+ setRefreshCredentialMetadata,
+} from '@package/agent/src/openid4vc/refreshMetadata'
+import { updateCredential } from '@package/agent/src/storage/credential'
import { pidSchemes } from '../constants'
import { ReceivePidUseCaseFlow } from './ReceivePidUseCaseFlow'
import { C_PRIME_SD_JWT_MDOC_OFFER } from './bdrPidIssuerOffers'
@@ -55,9 +56,11 @@ export class RefreshPidUseCase {
public async retrieveCredentialsUsingExistingRecords({
sdJwt,
mdoc,
+ batchSize = 2,
}: {
sdJwt?: SdJwtVcRecord
mdoc?: MdocRecord
+ batchSize?: number
}) {
const existingRefreshMetadata =
(sdJwt ? getRefreshCredentialMetadata(sdJwt) : undefined) ??
@@ -94,7 +97,7 @@ export class RefreshPidUseCase {
resolvedCredentialOffer: this.resolvedCredentialOffer,
credentialConfigurationIdsToRequest,
clientId: RefreshPidUseCase.CLIENT_ID,
- requestBatch: 2,
+ requestBatch: batchSize,
pidSchemes,
})
diff --git a/packages/agent/src/batch.ts b/packages/agent/src/batch.ts
index cf921916..cec7c4cf 100644
--- a/packages/agent/src/batch.ts
+++ b/packages/agent/src/batch.ts
@@ -1,9 +1,31 @@
import { Mdoc, MdocRecord, SdJwtVcRecord, W3cCredentialRecord } from '@credo-ts/core'
-import type { EitherAgent } from './agent'
+import type { AppAgent } from '../../../apps/easypid/src/agent'
+import { RefreshPidUseCase } from '../../../apps/easypid/src/use-cases/RefreshPidUseCase'
+import type { EasyPIDAppAgent, EitherAgent } from './agent'
+import { getCredentialCategoryMetadata } from './credentialCategoryMetadata'
import { decodeW3cCredential } from './format/credentialEncoding'
import { getBatchCredentialMetadata } from './openid4vc/batchMetadata'
+import { getRefreshCredentialMetadata } from './openid4vc/refreshMetadata'
import { updateCredential } from './storage'
+export async function refreshPid({
+ agent,
+ sdJwt,
+ mdoc,
+ batchSize,
+}: { agent: AppAgent; sdJwt?: SdJwtVcRecord; mdoc?: MdocRecord; batchSize?: number }) {
+ console.log('refreshing PID')
+ const useCase = await RefreshPidUseCase.initialize({
+ agent,
+ })
+
+ await useCase.retrieveCredentialsUsingExistingRecords({
+ sdJwt,
+ mdoc,
+ batchSize,
+ })
+}
+
/**
* If available, takes a credential from the batch.
*
@@ -14,31 +36,59 @@ export async function handleBatchCredential {
const batchMetadata = getBatchCredentialMetadata(credentialRecord)
+ if (!batchMetadata) return credentialRecord
+
+ // TODO: maybe we should also store the main credential in the additional credentials (and rename it)
+ // As right now the main one is mostly for display
+ const batchCredential = batchMetadata.additionalCredentials.pop()
+
+ // Store the record with the used credential removed. Even if the presentation fails we remove it, as we want to be careful
+ // if the presentation was shared
+ if (batchCredential) await updateCredential(agent, credentialRecord)
- if (batchMetadata) {
- const batchCredential = batchMetadata.additionalCredentials.pop()
-
- if (batchCredential) {
- // Store the record with the used credential removed. Even if the presentation fails we remove it, as we want to be careful
- // if the presentation was shared
- await updateCredential(agent, credentialRecord)
-
- if (credentialRecord instanceof MdocRecord) {
- return new MdocRecord({
- mdoc: Mdoc.fromBase64Url(batchCredential as string),
- }) as CredentialRecord
- }
- if (credentialRecord instanceof SdJwtVcRecord) {
- return new SdJwtVcRecord({
- compactSdJwtVc: batchCredential as string,
- }) as CredentialRecord
- }
- if (credentialRecord instanceof W3cCredentialRecord) {
- return new W3cCredentialRecord({
- tags: { expandedTypes: [] },
- credential: decodeW3cCredential(batchCredential),
- }) as CredentialRecord
- }
+ // Try to refresh the pid when we run out
+ // TODO: we should probably move this somewhere else at some point
+ const categoryMetadata = getCredentialCategoryMetadata(credentialRecord)
+ const refreshMetadata = getRefreshCredentialMetadata(credentialRecord)
+ if (
+ categoryMetadata?.credentialCategory === 'DE-PID' &&
+ refreshMetadata &&
+ batchMetadata.additionalCredentials.length === 0
+ ) {
+ refreshPid({
+ agent: agent as EasyPIDAppAgent,
+ sdJwt: credentialRecord.type === 'SdJwtVcRecord' ? credentialRecord : undefined,
+ mdoc: credentialRecord.type === 'MdocRecord' ? credentialRecord : undefined,
+ // Get a batch of 5 for a single record type
+ batchSize: 5,
+ })
+ .catch((error) => {
+ // TODO: we should handle the case where the refresh token is expired
+ agent.config.logger.error('Error refreshing pid', {
+ error,
+ })
+ })
+ .then(() => {
+ agent.config.logger.debug('Successfully refreshed PID')
+ })
+ }
+
+ if (batchCredential) {
+ if (credentialRecord instanceof MdocRecord) {
+ return new MdocRecord({
+ mdoc: Mdoc.fromBase64Url(batchCredential as string),
+ }) as CredentialRecord
+ }
+ if (credentialRecord instanceof SdJwtVcRecord) {
+ return new SdJwtVcRecord({
+ compactSdJwtVc: batchCredential as string,
+ }) as CredentialRecord
+ }
+ if (credentialRecord instanceof W3cCredentialRecord) {
+ return new W3cCredentialRecord({
+ tags: { expandedTypes: [] },
+ credential: decodeW3cCredential(batchCredential),
+ }) as CredentialRecord
}
}
diff --git a/packages/agent/src/invitation/handler.ts b/packages/agent/src/invitation/handler.ts
index 98978eac..584f882c 100644
--- a/packages/agent/src/invitation/handler.ts
+++ b/packages/agent/src/invitation/handler.ts
@@ -44,7 +44,6 @@ import { filter, first, firstValueFrom, merge, timeout } from 'rxjs'
import { Oauth2Client, getAuthorizationServerMetadataFromList } from '@animo-id/oauth2'
import q from 'query-string'
-import { handleBatchCredential } from '../batch'
import { credentialRecordFromCredential, encodeCredential } from '../format/credentialEncoding'
import {
type FormattedSubmission,
@@ -536,96 +535,6 @@ export const getCredentialsForProofRequest = async ({
} as const
}
-export const shareProof = async ({
- agent,
- resolvedRequest,
- selectedCredentials,
-}: {
- agent: EitherAgent
- resolvedRequest: CredentialsForProofRequest
- selectedCredentials: { [inputDescriptorId: string]: string }
-}) => {
- const { authorizationRequest } = resolvedRequest
- if (
- !resolvedRequest.credentialsForRequest?.areRequirementsSatisfied &&
- !resolvedRequest.queryResult?.canBeSatisfied
- ) {
- throw new Error('Requirements from proof request are not satisfied')
- }
-
- // Map all requirements and entries to a credential record. If a credential record for an
- // input descriptor has been provided in `selectedCredentials` we will use that. Otherwise
- // it will pick the first available credential.
- const presentationExchangeCredentials = resolvedRequest.credentialsForRequest
- ? Object.fromEntries(
- await Promise.all(
- resolvedRequest.credentialsForRequest.requirements.flatMap((requirement) =>
- requirement.submissionEntry.slice(0, requirement.needsCount).map(async (entry) => {
- const credentialId = selectedCredentials[entry.inputDescriptorId]
- const credential =
- entry.verifiableCredentials.find((vc) => vc.credentialRecord.id === credentialId) ??
- entry.verifiableCredentials[0]
-
- // Optionally use a batch credential
- const credentialRecord = await handleBatchCredential(agent, credential.credentialRecord)
-
- return [entry.inputDescriptorId, [credentialRecord]] as [string, (typeof credentialRecord)[]]
- })
- )
- )
- )
- : undefined
-
- // TODO: support credential selection for DCQL
- const dcqlCredentials = resolvedRequest.queryResult
- ? Object.fromEntries(
- await Promise.all(
- Object.entries(
- agent.modules.openId4VcHolder.selectCredentialsForDcqlRequest(resolvedRequest.queryResult)
- ).map(async ([queryCredentialId, credential]) => {
- // Optionally use a batch credential
- const credentialRecord = await handleBatchCredential(agent, credential.credentialRecord)
-
- return [queryCredentialId, { ...credential, credentialRecord }]
- })
- )
- )
- : undefined
-
- try {
- const result = await agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({
- authorizationRequest,
- presentationExchange: presentationExchangeCredentials
- ? {
- credentials: presentationExchangeCredentials,
- }
- : undefined,
- dcql: dcqlCredentials
- ? {
- credentials: dcqlCredentials,
- }
- : undefined,
- })
-
- // if redirect_uri is provided, open it in the browser
- // Even if the response returned an error, we must open this uri
- if (result.redirectUri) {
- await Linking.openURL(result.redirectUri)
- }
-
- if (result.serverResponse.status < 200 || result.serverResponse.status > 299) {
- throw new Error(
- `Error while accepting authorization request. ${JSON.stringify(result.serverResponse.body, null, 2)}`
- )
- }
-
- return result
- } catch (error) {
- // Handle biometric authentication errors
- throw BiometricAuthenticationError.tryParseFromError(error) ?? error
- }
-}
-
/**
* @todo we probably need a way to cancel this method, if the qr scanner is .e.g dismissed.
*/
diff --git a/packages/agent/src/invitation/index.ts b/packages/agent/src/invitation/index.ts
index 4908fe99..35c52ff0 100644
--- a/packages/agent/src/invitation/index.ts
+++ b/packages/agent/src/invitation/index.ts
@@ -35,8 +35,8 @@ export {
acquirePreAuthorizedAccessToken,
resolveOpenId4VciOffer,
getCredentialsForProofRequest,
- shareProof,
withTrustedCertificate,
acquireAuthorizationCodeUsingPresentation,
} from './handler'
+export { shareProof } from './shareProof'
export * from './error'
diff --git a/packages/agent/src/invitation/shareProof.ts b/packages/agent/src/invitation/shareProof.ts
new file mode 100644
index 00000000..c1cc91d5
--- /dev/null
+++ b/packages/agent/src/invitation/shareProof.ts
@@ -0,0 +1,95 @@
+import { Linking } from 'react-native'
+import type { EitherAgent } from '../agent'
+import { handleBatchCredential } from '../batch'
+import { BiometricAuthenticationError } from './error'
+import type { CredentialsForProofRequest } from './handler'
+
+export const shareProof = async ({
+ agent,
+ resolvedRequest,
+ selectedCredentials,
+}: {
+ agent: EitherAgent
+ resolvedRequest: CredentialsForProofRequest
+ selectedCredentials: { [inputDescriptorId: string]: string }
+}) => {
+ const { authorizationRequest } = resolvedRequest
+ if (
+ !resolvedRequest.credentialsForRequest?.areRequirementsSatisfied &&
+ !resolvedRequest.queryResult?.canBeSatisfied
+ ) {
+ throw new Error('Requirements from proof request are not satisfied')
+ }
+
+ // Map all requirements and entries to a credential record. If a credential record for an
+ // input descriptor has been provided in `selectedCredentials` we will use that. Otherwise
+ // it will pick the first available credential.
+ const presentationExchangeCredentials = resolvedRequest.credentialsForRequest
+ ? Object.fromEntries(
+ await Promise.all(
+ resolvedRequest.credentialsForRequest.requirements.flatMap((requirement) =>
+ requirement.submissionEntry.slice(0, requirement.needsCount).map(async (entry) => {
+ const credentialId = selectedCredentials[entry.inputDescriptorId]
+ const credential =
+ entry.verifiableCredentials.find((vc) => vc.credentialRecord.id === credentialId) ??
+ entry.verifiableCredentials[0]
+
+ // Optionally use a batch credential
+ const credentialRecord = await handleBatchCredential(agent, credential.credentialRecord)
+
+ return [entry.inputDescriptorId, [credentialRecord]] as [string, (typeof credentialRecord)[]]
+ })
+ )
+ )
+ )
+ : undefined
+
+ // TODO: support credential selection for DCQL
+ const dcqlCredentials = resolvedRequest.queryResult
+ ? Object.fromEntries(
+ await Promise.all(
+ Object.entries(
+ agent.modules.openId4VcHolder.selectCredentialsForDcqlRequest(resolvedRequest.queryResult)
+ ).map(async ([queryCredentialId, credential]) => {
+ // Optionally use a batch credential
+ const credentialRecord = await handleBatchCredential(agent, credential.credentialRecord)
+
+ return [queryCredentialId, { ...credential, credentialRecord }]
+ })
+ )
+ )
+ : undefined
+
+ try {
+ const result = await agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({
+ authorizationRequest,
+ presentationExchange: presentationExchangeCredentials
+ ? {
+ credentials: presentationExchangeCredentials,
+ }
+ : undefined,
+ dcql: dcqlCredentials
+ ? {
+ credentials: dcqlCredentials,
+ }
+ : undefined,
+ })
+
+ // if redirect_uri is provided, open it in the browser
+ // Even if the response returned an error, we must open this uri
+ if (result.redirectUri) {
+ await Linking.openURL(result.redirectUri)
+ }
+
+ if (result.serverResponse.status < 200 || result.serverResponse.status > 299) {
+ throw new Error(
+ `Error while accepting authorization request. ${JSON.stringify(result.serverResponse.body, null, 2)}`
+ )
+ }
+
+ return result
+ } catch (error) {
+ // Handle biometric authentication errors
+ throw BiometricAuthenticationError.tryParseFromError(error) ?? error
+ }
+}