From 942b8335cbe8d6ce93f40d8cc6bbdf9103c1320c Mon Sep 17 00:00:00 2001 From: wadeking98 Date: Tue, 7 Nov 2023 16:07:35 -0800 Subject: [PATCH] added support for remote proof bundles Signed-off-by: wadeking98 --- .../core/App/contexts/configuration.tsx | 1 + .../core/App/hooks/proof-request-templates.ts | 165 +++++++++++++++++- .../core/App/screens/ProofRequestDetails.tsx | 14 +- .../core/App/screens/ProofRequesting.tsx | 9 +- .../screens/ProofRequestDetails.test.tsx | 44 ++++- 5 files changed, 211 insertions(+), 22 deletions(-) diff --git a/packages/legacy/core/App/contexts/configuration.tsx b/packages/legacy/core/App/contexts/configuration.tsx index ff076148e1..dbded1b357 100644 --- a/packages/legacy/core/App/contexts/configuration.tsx +++ b/packages/legacy/core/App/contexts/configuration.tsx @@ -31,6 +31,7 @@ export interface ConfigurationContext { credentialEmptyList: React.FC developer: React.FC OCABundleResolver: OCABundleResolverType + proofTemplateBaseUrl?: string scan: React.FC useBiometry: React.FC record: React.FC diff --git a/packages/legacy/core/App/hooks/proof-request-templates.ts b/packages/legacy/core/App/hooks/proof-request-templates.ts index 33a2bd94a9..69dc4ae090 100644 --- a/packages/legacy/core/App/hooks/proof-request-templates.ts +++ b/packages/legacy/core/App/hooks/proof-request-templates.ts @@ -1,18 +1,167 @@ -import { ProofRequestTemplate } from '../../verifier' +import axios from 'axios' +import { useEffect, useState } from 'react' + +import { AnonCredsProofRequestTemplatePayload, ProofRequestTemplate, useProofRequestTemplates } from '../../verifier' import { useConfiguration } from '../contexts/configuration' import { useStore } from '../contexts/store' +export interface ProofBundleResolverType { + resolve: (acceptDevRestrictions: boolean) => Promise + resolveById: (templateId: string, acceptDevRestrictions: boolean) => Promise +} + +const useRemoteProofBundleResolver = (indexFileBaseUrl: string | undefined): ProofBundleResolverType => { + if (indexFileBaseUrl) { + return new RemoteProofBundleResolver(indexFileBaseUrl) + } else { + return new DefaultProofBundleResolver() + } +} + +const calculatePreviousYear = (yearOffset: number) => { + const pastDate = new Date() + pastDate.setFullYear(pastDate.getFullYear() + yearOffset) + return parseInt(pastDate.toISOString().split('T')[0].replace(/-/g, '')) +} + +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)] + + markers.forEach((marker) => { + const markerValue = markerActions[marker[1] as string](marker[2]) + templateString = templateString.replace(marker[0], markerValue) + }) + return JSON.parse(templateString) +} + +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 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 { + 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 { + 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 { + return Promise.resolve(this.proofRequestTemplates(acceptDevRestrictions)) + } + public async resolveById( + templateId: string, + acceptDevRestrictions: boolean + ): Promise { + return Promise.resolve( + this.proofRequestTemplates(acceptDevRestrictions).find((template) => template.id === templateId) + ) + } +} + export const useTemplates = (): Array => { const [store] = useStore() - const { proofRequestTemplates } = useConfiguration() - return (proofRequestTemplates && proofRequestTemplates(store.preferences.acceptDevCredentials)) || [] + const [proofRequestTemplates, setProofRequestTemplates] = useState([]) + 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(undefined) + const { proofTemplateBaseUrl } = useConfiguration() + const resolver = useRemoteProofBundleResolver(proofTemplateBaseUrl) + useEffect(() => { + resolver.resolveById(templateId, store.preferences.acceptDevCredentials).then((template) => { + if (template) { + setProofRequestTemplate(applyTemplateMarkers(template)) + } + }) + }, []) + return proofRequestTemplate } diff --git a/packages/legacy/core/App/screens/ProofRequestDetails.tsx b/packages/legacy/core/App/screens/ProofRequestDetails.tsx index 0c79492a7f..1d361cb162 100644 --- a/packages/legacy/core/App/screens/ProofRequestDetails.tsx +++ b/packages/legacy/core/App/screens/ProofRequestDetails.tsx @@ -257,11 +257,11 @@ const ProofRequestDetails: React.FC = ({ 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) => { @@ -282,7 +282,7 @@ const ProofRequestDetails: React.FC = ({ route, naviga setMeta(metaOverlay) setAttributes(attributes) }) - }, [templateId]) + }, [templateId, template]) const onlyNumberRegex = /^\d+$/ @@ -305,6 +305,10 @@ const ProofRequestDetails: React.FC = ({ route, naviga ) const useProofRequest = useCallback(async () => { + if (!template) { + return + } + if (invalidPredicate) { setInvalidPredicate({ visible: true, predicate: invalidPredicate.predicate }) return @@ -323,7 +327,7 @@ const ProofRequestDetails: React.FC = ({ 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 }) diff --git a/packages/legacy/core/App/screens/ProofRequesting.tsx b/packages/legacy/core/App/screens/ProofRequesting.tsx index 47b2fa05fd..5278f82009 100644 --- a/packages/legacy/core/App/screens/ProofRequesting.tsx +++ b/packages/legacy/core/App/screens/ProofRequesting.tsx @@ -123,10 +123,6 @@ const ProofRequesting: React.FC = ({ route, navigation }) }, }) - if (!template) { - throw new Error('Unable to find proof request template') - } - const createProofRequest = useCallback(async () => { try { setMessage(undefined) @@ -165,6 +161,9 @@ const ProofRequesting: React.FC = ({ 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 @@ -181,7 +180,7 @@ const ProofRequesting: React.FC = ({ route, navigation }) } } sendAsyncProof() - }, [record]) + }, [record, template]) useEffect(() => { if (proofRecord && (isPresentationReceived(proofRecord) || isPresentationFailed(proofRecord))) { diff --git a/packages/legacy/core/__tests__/screens/ProofRequestDetails.test.tsx b/packages/legacy/core/__tests__/screens/ProofRequestDetails.test.tsx index 9186f298f4..e52d3188b9 100644 --- a/packages/legacy/core/__tests__/screens/ProofRequestDetails.test.tsx +++ b/packages/legacy/core/__tests__/screens/ProofRequestDetails.test.tsx @@ -8,7 +8,8 @@ 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' jest.mock('react-native-permissions', () => require('react-native-permissions/mock')) jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo) @@ -23,12 +24,47 @@ 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(), +})) + +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: [ + { + name: 'student_first_name', + restrictions:[{ cred_def_id: 'XUxBrVSALWHLeycAUhrNr9:3:CL:26293:student_card' }], + }, + { + name: 'student_last_name', + restrictions:[{ cred_def_id: 'XUxBrVSALWHLeycAUhrNr9:3:CL:26293:student_card' }], + }, + ], + }, + ], + }, + } +] +// @ts-ignore +useTemplates.mockImplementation(() => templates) +// @ts-ignore +useTemplate.mockImplementation((id) => templates[0]) const templateId = templates[0].id const connectionId = 'test' const navigation = useNavigation() @@ -50,7 +86,7 @@ describe('ProofRequestDetails Component', () => { test('Renders correctly', async () => { const tree = renderView({ templateId }) - await act(async () => {}) + await act(async () => { }) expect(tree).toMatchSnapshot() })