Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: remote proof bundles #1022

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/legacy/core/App/contexts/configuration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ConfigurationContext {
credentialEmptyList: React.FC<EmptyListProps>
developer: React.FC
OCABundleResolver: OCABundleResolverType
proofTemplateBaseUrl?: string
scan: React.FC<ScanProps>
useBiometry: React.FC
record: React.FC<RecordProps>
Expand Down
32 changes: 25 additions & 7 deletions packages/legacy/core/App/hooks/proof-request-templates.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import { useEffect, useState } from 'react'

import { ProofRequestTemplate } from '../../verifier'
import { useConfiguration } from '../contexts/configuration'
import { useStore } from '../contexts/store'
import { applyTemplateMarkers, useRemoteProofBundleResolver } from '../utils/proofBundle'

export const useTemplates = (): Array<ProofRequestTemplate> => {
const [store] = useStore()
const { proofRequestTemplates } = useConfiguration()
return (proofRequestTemplates && proofRequestTemplates(store.preferences.acceptDevCredentials)) || []
const [proofRequestTemplates, setProofRequestTemplates] = useState<ProofRequestTemplate[]>([])
const { proofTemplateBaseUrl } = useConfiguration()
const resolver = useRemoteProofBundleResolver(proofTemplateBaseUrl)
useEffect(() => {
resolver.resolve(store.preferences.acceptDevCredentials).then((templates) => {
if (templates) {
setProofRequestTemplates(applyTemplateMarkers(templates))
}
})
}, [])
return proofRequestTemplates
}

export const useTemplate = (templateId: string): ProofRequestTemplate | undefined => {
const { proofRequestTemplates } = useConfiguration()
const [store] = useStore()
return (
proofRequestTemplates &&
proofRequestTemplates(store.preferences.acceptDevCredentials).find((template) => template.id === templateId)
)
const [proofRequestTemplate, setProofRequestTemplate] = useState<ProofRequestTemplate | undefined>(undefined)
const { proofTemplateBaseUrl } = useConfiguration()
const resolver = useRemoteProofBundleResolver(proofTemplateBaseUrl)
useEffect(() => {
resolver.resolveById(templateId, store.preferences.acceptDevCredentials).then((template) => {
if (template) {
setProofRequestTemplate(applyTemplateMarkers(template))
}
})
}, [])
return proofRequestTemplate
}
14 changes: 9 additions & 5 deletions packages/legacy/core/App/screens/ProofRequestDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,11 @@ const ProofRequestDetails: React.FC<ProofRequestDetailsProps> = ({ route, naviga
>(undefined)

const template = useTemplate(templateId)
if (!template) {
throw new Error('Unable to find proof request template')
}

useEffect(() => {
if (!template) {
return
}
const attributes = template.payload.type === ProofRequestType.AnonCreds ? template.payload.data : []

OCABundleResolver.resolve({ identifiers: { templateId }, language: i18n.language }).then((bundle) => {
Expand All @@ -282,7 +282,7 @@ const ProofRequestDetails: React.FC<ProofRequestDetailsProps> = ({ route, naviga
setMeta(metaOverlay)
setAttributes(attributes)
})
}, [templateId])
}, [templateId, template])

const onlyNumberRegex = /^\d+$/

Expand All @@ -305,6 +305,10 @@ const ProofRequestDetails: React.FC<ProofRequestDetailsProps> = ({ route, naviga
)

const useProofRequest = useCallback(async () => {
if (!template) {
return
}

if (invalidPredicate) {
setInvalidPredicate({ visible: true, predicate: invalidPredicate.predicate })
return
Expand All @@ -323,7 +327,7 @@ const ProofRequestDetails: React.FC<ProofRequestDetailsProps> = ({ route, naviga
// Else redirect to the screen with connectionless request
navigation.navigate(Screens.ProofRequesting, { templateId, predicateValues: customPredicateValues })
}
}, [agent, templateId, connectionId, customPredicateValues, invalidPredicate])
}, [agent, template, templateId, connectionId, customPredicateValues, invalidPredicate])

const showTemplateUsageHistory = useCallback(async () => {
navigation.navigate(Screens.ProofRequestUsageHistory, { templateId })
Expand Down
9 changes: 4 additions & 5 deletions packages/legacy/core/App/screens/ProofRequesting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,6 @@ const ProofRequesting: React.FC<ProofRequestingProps> = ({ route, navigation })
},
})

if (!template) {
throw new Error('Unable to find proof request template')
}

const createProofRequest = useCallback(async () => {
try {
setMessage(undefined)
Expand Down Expand Up @@ -165,6 +161,9 @@ const ProofRequesting: React.FC<ProofRequestingProps> = ({ route, navigation })
}, [isFocused])

useEffect(() => {
if (!template) {
return
}
const sendAsyncProof = async () => {
if (record && record.state === DidExchangeState.Completed) {
//send haptic feedback to verifier that connection is completed
Expand All @@ -181,7 +180,7 @@ const ProofRequesting: React.FC<ProofRequestingProps> = ({ route, navigation })
}
}
sendAsyncProof()
}, [record])
}, [record, template])

useEffect(() => {
if (proofRecord && (isPresentationReceived(proofRecord) || isPresentationFailed(proofRecord))) {
Expand Down
135 changes: 135 additions & 0 deletions packages/legacy/core/App/utils/proofBundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import axios from 'axios'

import { AnonCredsProofRequestTemplatePayload, ProofRequestTemplate, useProofRequestTemplates } from '../../verifier'
import { useConfiguration } from '../contexts/configuration'

const calculatePreviousYear = (yearOffset: number) => {
const pastDate = new Date()
pastDate.setFullYear(pastDate.getFullYear() + yearOffset)
return parseInt(pastDate.toISOString().split('T')[0].replace(/-/g, ''))
}

export const applyTemplateMarkers = (templates: any): any => {
if (!templates) return templates
const markerActions: { [key: string]: (param: string) => string } = {
now: () => Math.floor(new Date().getTime() / 1000).toString(),
currentDate: (offset: string) => calculatePreviousYear(parseInt(offset)).toString(),
}
let templateString = JSON.stringify(templates)
const markers = [...templateString.matchAll(/"@\{(\w+)(?:\((\S*)\))?\}"/gm)]
wadeking98 marked this conversation as resolved.
Show resolved Hide resolved

markers.forEach((marker) => {
const markerValue = markerActions[marker[1] as string](marker[2])
templateString = templateString.replace(marker[0], markerValue)
})
return JSON.parse(templateString)
}

export const applyDevRestrictions = (templates: ProofRequestTemplate[]): ProofRequestTemplate[] => {
return templates.map((temp) => {
return {
...temp,
payload: {
...temp.payload,
data: (temp.payload as AnonCredsProofRequestTemplatePayload).data.map((data) => {
return {
...data,
requestedAttributes: data.requestedAttributes?.map((attr) => {
return {
...attr,
restrictions: [...(attr.restrictions ?? []), ...(attr.devRestrictions ?? [])],
devRestrictions: [],
}
}),
requestedPredicates: data.requestedPredicates?.map((pred) => {
return {
...pred,
restrictions: [...(pred.restrictions ?? []), ...(pred.devRestrictions ?? [])],
devRestrictions: [],
}
}),
}
}),
},
}
})
}

export interface ProofBundleResolverType {
resolve: (acceptDevRestrictions: boolean) => Promise<ProofRequestTemplate[] | undefined>
resolveById: (templateId: string, acceptDevRestrictions: boolean) => Promise<ProofRequestTemplate | undefined>
}

export const useRemoteProofBundleResolver = (indexFileBaseUrl: string | undefined): ProofBundleResolverType => {
if (indexFileBaseUrl) {
return new RemoteProofBundleResolver(indexFileBaseUrl)
} else {
return new DefaultProofBundleResolver()
}
}

export class RemoteProofBundleResolver implements ProofBundleResolverType {
private remoteServer
private templateData: ProofRequestTemplate[] | undefined

public constructor(indexFileBaseUrl: string) {
this.remoteServer = axios.create({
baseURL: indexFileBaseUrl,
})
}
public async resolve(acceptDevRestrictions: boolean): Promise<ProofRequestTemplate[] | undefined> {
if (this.templateData) {
let templateData = this.templateData
if (acceptDevRestrictions) {
templateData = applyDevRestrictions(templateData)
}
return Promise.resolve(templateData)
}
return this.remoteServer.get('proof-templates.json').then((response) => {
try {
let templateData: ProofRequestTemplate[] = response.data
this.templateData = templateData
if (acceptDevRestrictions) {
templateData = applyDevRestrictions(templateData)
}
return templateData
} catch (error) {
return undefined
}
})
}
public async resolveById(
templateId: string,
acceptDevRestrictions: boolean
): Promise<ProofRequestTemplate | undefined> {
if (!this.templateData) {
return (await this.resolve(acceptDevRestrictions))?.find((template) => template.id === templateId)
} else {
let templateData = this.templateData
if (acceptDevRestrictions) {
templateData = applyDevRestrictions(templateData)
}
const template = templateData.find((template) => template.id === templateId)
return template
}
}
}

export class DefaultProofBundleResolver implements ProofBundleResolverType {
private proofRequestTemplates
public constructor() {
const { proofRequestTemplates } = useConfiguration()
this.proofRequestTemplates = proofRequestTemplates ?? useProofRequestTemplates
}
public async resolve(acceptDevRestrictions: boolean): Promise<ProofRequestTemplate[]> {
return Promise.resolve(this.proofRequestTemplates(acceptDevRestrictions))
}
public async resolveById(
templateId: string,
acceptDevRestrictions: boolean
): Promise<ProofRequestTemplate | undefined> {
return Promise.resolve(
this.proofRequestTemplates(acceptDevRestrictions).find((template) => template.id === templateId)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import React from 'react'
import { ConfigurationContext } from '../../App/contexts/configuration'
import { NetworkProvider } from '../../App/contexts/network'
import configurationContext from '../contexts/configuration'
import { useProofRequestTemplates } from '../../verifier/request-templates'
import ProofRequestDetails from '../../App/screens/ProofRequestDetails'
import { testIdWithKey } from '../../App'
import { ProofRequestType, testIdWithKey } from '../../App'
import { useTemplates, useTemplate } from '../../App/hooks/proof-request-templates'
import axios from 'axios'
import { applyTemplateMarkers, useRemoteProofBundleResolver } from '../../App/utils/proofBundle'

jest.mock('react-native-permissions', () => require('react-native-permissions/mock'))
jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo)
Expand All @@ -23,12 +25,60 @@ jest.mock('@react-navigation/native', () => {
return require('../../__mocks__/custom/@react-navigation/native')
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
jest.mock('react-native-localize', () => {})
jest.mock('react-native-localize', () => { })
jest.mock('react-native-device-info', () => () => jest.fn())

jest.useFakeTimers({ legacyFakeTimers: true })
jest.spyOn(global, 'setTimeout')
const templates = useProofRequestTemplates(false)

jest.mock('../../App/hooks/proof-request-templates', () => ({
useTemplates: jest.fn(),
useTemplate: jest.fn(),
}))

jest.mock('axios', () => ({ create: jest.fn() }))


const templates = [
{
id: 'Aries:5:StudentFullName:0.0.1:indy',
name: 'Student full name',
description: 'Verify the full name of a student',
version: '0.0.1',
payload: {
type: ProofRequestType.AnonCreds,
data: [
{
schema: 'XUxBrVSALWHLeycAUhrNr9:3:CL:26293:Student Card',
requestedAttributes: [
{
names: ['student_first_name', 'student_last_name'],
restrictions: [{ cred_def_id: 'XUxBrVSALWHLeycAUhrNr9:3:CL:26293:student_card' }],
devRestrictions: [{ schema_name: 'student_card' }],
non_revoked: { to: "@{now}" },
},
],
requestedPredicates: [
{
name: 'expiry_date',
predicateType: '>=',
predicateValue: "@{currentDate(0)}",
restrictions: [{ cred_def_id: 'XUxBrVSALWHLeycAUhrNr9:3:CL:26293:student_card' }],
devRestrictions: [{ schema_name: 'student_card' }],
},
],
},
],
},
}
]

// @ts-ignore
axios.create.mockImplementation(() => ({ get: () => Promise.resolve({ data: templates }) }))
// @ts-ignore
useTemplates.mockImplementation(() => templates)
// @ts-ignore
useTemplate.mockImplementation((id) => templates[0])
const templateId = templates[0].id
const connectionId = 'test'
const navigation = useNavigation()
Expand All @@ -48,9 +98,22 @@ describe('ProofRequestDetails Component', () => {
)
}

test('Proof bundle resolver works correctly', async () => {
const resolver = useRemoteProofBundleResolver("http://localhost:3000")
const bundle = await resolver.resolve(true)
expect((bundle?.[0].payload.data[0] as any).requestedAttributes[0].restrictions.length).toBe(2)
})

test("Template is parsed correctly", async () => {
const template = templates[0]
const parsedTemplate = applyTemplateMarkers(template)
expect(parsedTemplate.payload.data[0].requestedAttributes[0].non_revoked.to).not.toBe("@{now}")
expect(parsedTemplate.payload.data[0].requestedPredicates[0].predicateValue.to).not.toBe("@{currentDate(0)}")
})

test('Renders correctly', async () => {
const tree = renderView({ templateId })
await act(async () => {})
await act(async () => { })
expect(tree).toMatchSnapshot()
})

Expand Down
Loading