From f91184f7114df12d9083e22b5bbef14527387be0 Mon Sep 17 00:00:00 2001 From: Bryce McMath Date: Thu, 18 Jan 2024 20:22:36 -0800 Subject: [PATCH 1/3] fix: handle multi use invitations Signed-off-by: Bryce McMath --- packages/legacy/core/App/index.ts | 1 + .../legacy/core/App/screens/Connection.tsx | 6 + packages/legacy/core/App/utils/helpers.ts | 24 +++ .../connection-v1-response-received.json | 18 ++ .../__tests__/screens/Connection.test.tsx | 28 ++- .../__snapshots__/Connection.test.tsx.snap | 191 +++++++++++++++++- 6 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 packages/legacy/core/__tests__/fixtures/connection-v1-response-received.json diff --git a/packages/legacy/core/App/index.ts b/packages/legacy/core/App/index.ts index 2af6e7b03f..491708cece 100644 --- a/packages/legacy/core/App/index.ts +++ b/packages/legacy/core/App/index.ts @@ -64,6 +64,7 @@ export { BifoldError } from './types/error' export { EventTypes } from './constants' export { didMigrateToAskar, migrateToAskar } from './utils/migration' export { createLinkSecretIfRequired, getAgentModules } from './utils/agent' +export { removeExistingInvitationIfRequired } from './utils/helpers' export type { AnimatedComponents } from './animated-components' export type { diff --git a/packages/legacy/core/App/screens/Connection.tsx b/packages/legacy/core/App/screens/Connection.tsx index 75a69eced6..941c827cc9 100644 --- a/packages/legacy/core/App/screens/Connection.tsx +++ b/packages/legacy/core/App/screens/Connection.tsx @@ -1,3 +1,4 @@ +import { DidExchangeState } from '@aries-framework/core' import { useConnectionById } from '@aries-framework/react-hooks' import { CommonActions, useFocusEffect } from '@react-navigation/native' import { StackScreenProps } from '@react-navigation/stack' @@ -136,6 +137,11 @@ const Connection: React.FC = ({ navigation, route }) => { }, [state.shouldShowDelayMessage]) useEffect(() => { + // for non-connectionless, invalid connections stay in the below state + if (connection && connection.state === DidExchangeState.RequestSent) { + return + } + if ( !connectionId && !oobRecord && diff --git a/packages/legacy/core/App/utils/helpers.ts b/packages/legacy/core/App/utils/helpers.ts index bb5c5b6f53..836637b5e3 100644 --- a/packages/legacy/core/App/utils/helpers.ts +++ b/packages/legacy/core/App/utils/helpers.ts @@ -810,6 +810,28 @@ export const receiveMessageFromDeepLink = async (url: string, agent: Agent | und return message } +/** + * + * @param agent an Agent instance + * @param invitationId id of invitation + */ +export const removeExistingInvitationIfRequired = async ( + agent: Agent | undefined, + invitationId: string +): Promise => { + try { + // If something fails before we get the credential we need to + // cleanup the old invitation before it can be used again. + const oobRecord = await agent?.oob.findByReceivedInvitationId(invitationId) + if (oobRecord) { + await agent?.oob.deleteById(oobRecord.id) + } + } catch (error) { + // findByInvitationId with throw if unsuccessful but that's not a problem. + // It just means there is nothing to delete. + } +} + /** * * @param uri a URI containing a base64 encoded connection invite in the query parameter @@ -843,7 +865,9 @@ export const connectFromInvitation = async ( // don't throw an error, will try to connect again below } } + if (!record) { + await removeExistingInvitationIfRequired(agent, invitation.id) record = await agent?.oob.receiveInvitation(invitation, { reuseConnection }) } diff --git a/packages/legacy/core/__tests__/fixtures/connection-v1-response-received.json b/packages/legacy/core/__tests__/fixtures/connection-v1-response-received.json new file mode 100644 index 0000000000..c6fceb1984 --- /dev/null +++ b/packages/legacy/core/__tests__/fixtures/connection-v1-response-received.json @@ -0,0 +1,18 @@ +{ + "_tags": {}, + "metadata": {}, + "connectionTypes": [], + "id": "43be26c3-4b24-4915-9a84-367c5aac5bca", + "createdAt": "2023-06-30T18:44:29.268Z", + "did": "did:peer:1zQmYDZQFXdHuZam9e1kNtEevQjL9boEBGszBDGX2N8X8xeQ", + "invitationDid": "did:peer:2.SeyJzIjoiaHR0cHM6Ly90cmFjdGlvbi1kdHMtYWNhcHktZGV2LmFwcHMuc2lsdmVyLmRldm9wcy5nb3YuYmMuY2EiLCJ0IjoiZGlkLWNvbW11bmljYXRpb24iLCJwcmlvcml0eSI6MCwicmVjaXBpZW50S2V5cyI6WyJkaWQ6a2V5Ono2TWtuY3B2aG9GUXdnendyQTNRQXk3a1ZkYlB5d0I5c1FzUjFuTGszN3VKVmltSCN6Nk1rbmNwdmhvRlF3Z3p3ckEzUUF5N2tWZGJQeXdCOXNRc1IxbkxrMzd1SlZpbUgiXX0", + "theirLabel": "BestBC College", + "state": "response-received", + "role": "requester", + "autoAcceptConnection": true, + "threadId": "5f35f941-854a-4f76-ad51-5ef7f16b238c", + "mediatorId": "5497adc9-01d4-4f39-a9a8-b947dc662d5d", + "protocol": "https://didcomm.org/connections/1.0", + "outOfBandId": "99c18620-ec05-4b81-b569-a8065d62263f", + "updatedAt": "2023-06-30T18:44:29.272Z" +} \ No newline at end of file diff --git a/packages/legacy/core/__tests__/screens/Connection.test.tsx b/packages/legacy/core/__tests__/screens/Connection.test.tsx index 72d9f461e7..c83e44dd3f 100644 --- a/packages/legacy/core/__tests__/screens/Connection.test.tsx +++ b/packages/legacy/core/__tests__/screens/Connection.test.tsx @@ -19,6 +19,8 @@ const proofNotifPath = path.join(__dirname, '../fixtures/proof-notif.json') const proofNotif = JSON.parse(fs.readFileSync(proofNotifPath, 'utf8')) const connectionPath = path.join(__dirname, '../fixtures/connection-v1.json') const connection = JSON.parse(fs.readFileSync(connectionPath, 'utf8')) +const connectionResponseReceivedPath = path.join(__dirname, '../fixtures/connection-v1-response-received.json') +const connectionResponseReceived = JSON.parse(fs.readFileSync(connectionResponseReceivedPath, 'utf8')) const outOfBandInvitation = { goalCode: 'aries.vc.verify.once' } const props = { params: { connectionId: connection.id } } @@ -134,7 +136,7 @@ describe('ConnectionModal Component', () => { expect(tree).toMatchSnapshot() }) - test('Connection with no goal code', async () => { + test('Connection with no goal code, request-sent state', async () => { const navigation = useNavigation() // @ts-ignore-next-line useNotifications.mockReturnValue({ total: 1, notifications: [proofNotif] }) @@ -152,11 +154,33 @@ describe('ConnectionModal Component', () => { const tree = render(element) + expect(tree).toMatchSnapshot() + expect(navigation.getParent()?.dispatch).not.toBeCalled() + }) + + test('Connection with no goal code, response-received state', async () => { + const navigation = useNavigation() + // @ts-ignore-next-line + useNotifications.mockReturnValue({ total: 1, notifications: [proofNotif] }) + // @ts-ignore-next-line + useOutOfBandByConnectionId.mockReturnValue({ outOfBandInvitation: {} }) + // @ts-ignore-next-line + useConnectionById.mockReturnValue(connectionResponseReceived) + // @ts-ignore-next-line + useProofById.mockReturnValue(proofNotif) + const element = ( + + + + ) + + const tree = render(element) + expect(tree).toMatchSnapshot() expect(navigation.getParent()?.dispatch).toBeCalledTimes(1) expect(CommonActions.reset).toBeCalledWith({ index: 1, - routes: [{ name: 'Tab Stack' }, { name: 'Chat', params: { connectionId: connection.id } }], + routes: [{ name: 'Tab Stack' }, { name: 'Chat', params: { connectionId: connectionResponseReceived.id } }], }) }) diff --git a/packages/legacy/core/__tests__/screens/__snapshots__/Connection.test.tsx.snap b/packages/legacy/core/__tests__/screens/__snapshots__/Connection.test.tsx.snap index c7ef68aac4..cac910e9d6 100644 --- a/packages/legacy/core/__tests__/screens/__snapshots__/Connection.test.tsx.snap +++ b/packages/legacy/core/__tests__/screens/__snapshots__/Connection.test.tsx.snap @@ -1,6 +1,195 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConnectionModal Component Connection with no goal code 1`] = ` +exports[`ConnectionModal Component Connection with no goal code, request-sent state 1`] = ` + + + + + + + Connection.JustAMoment + + + + + < + fill="#FFFFFF" + height={130} + style={ + Object { + "position": "absolute", + } + } + width={130} + /> + + < + fill="#FFFFFF" + height={250} + width={250} + /> + + + + + + + + + + Loading.BackToHome + + + + + + +`; + +exports[`ConnectionModal Component Connection with no goal code, response-received state 1`] = ` Date: Thu, 18 Jan 2024 20:32:58 -0800 Subject: [PATCH 2/3] chore: update comments Signed-off-by: Bryce McMath --- packages/legacy/core/App/utils/helpers.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/legacy/core/App/utils/helpers.ts b/packages/legacy/core/App/utils/helpers.ts index 836637b5e3..2ce6f80ec0 100644 --- a/packages/legacy/core/App/utils/helpers.ts +++ b/packages/legacy/core/App/utils/helpers.ts @@ -811,7 +811,7 @@ export const receiveMessageFromDeepLink = async (url: string, agent: Agent | und } /** - * + * Useful for multi use invitations * @param agent an Agent instance * @param invitationId id of invitation */ @@ -820,15 +820,13 @@ export const removeExistingInvitationIfRequired = async ( invitationId: string ): Promise => { try { - // If something fails before we get the credential we need to - // cleanup the old invitation before it can be used again. const oobRecord = await agent?.oob.findByReceivedInvitationId(invitationId) if (oobRecord) { await agent?.oob.deleteById(oobRecord.id) } } catch (error) { - // findByInvitationId with throw if unsuccessful but that's not a problem. - // It just means there is nothing to delete. + // findByReceivedInvitationId will throw if unsuccessful but that's not a problem + // it just means there is nothing to delete } } From 20142385a0bc7b77f6a39777ff29834d295d59e7 Mon Sep 17 00:00:00 2001 From: Bryce McMath Date: Fri, 19 Jan 2024 14:29:47 -0800 Subject: [PATCH 3/3] chore: add tests for helper functions Signed-off-by: Bryce McMath --- .../core/__tests__/utils/helpers.test.ts | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/legacy/core/__tests__/utils/helpers.test.ts b/packages/legacy/core/__tests__/utils/helpers.test.ts index 07bb6a5dcd..d23ef9ce47 100644 --- a/packages/legacy/core/__tests__/utils/helpers.test.ts +++ b/packages/legacy/core/__tests__/utils/helpers.test.ts @@ -12,6 +12,8 @@ import { formatIfDate, formatTime, getConnectionName, + removeExistingInvitationIfRequired, + connectFromInvitation, } from '../../App/utils/helpers' const proofCredentialPath = path.join(__dirname, '../fixtures/proof-credential.json') @@ -99,7 +101,6 @@ describe('formatTime', () => { }) describe('formatIfDate', () => { - afterEach(() => { jest.clearAllMocks() }) @@ -111,12 +112,12 @@ describe('formatIfDate', () => { test('with format and string date', () => { const result = formatIfDate('YYYYMMDD', '20020523') - expect(result).toEqual("May 23, 2002") + expect(result).toEqual('May 23, 2002') }) test('with format and number date', () => { const result = formatIfDate('YYYYMMDD', 20020523) - expect(result).toEqual("May 23, 2002") + expect(result).toEqual('May 23, 2002') }) test('with format but invalid string date', () => { @@ -153,6 +154,49 @@ describe('createConnectionInvitation', () => { }) }) +describe('removeExistingInvitationIfRequired', () => { + test('without an existing oobRecord', async () => { + const { agent } = useAgent() + const invitationId = '1' + agent!.oob.findByReceivedInvitationId = jest + .fn() + .mockReturnValueOnce(Promise.reject('No received invitation with this id exists')) + const deleteById = jest.fn() + agent!.oob.deleteById = deleteById + + await removeExistingInvitationIfRequired(agent, invitationId) + + expect(deleteById).not.toBeCalled() + }) + test('with an existing oobRecord', async () => { + const { agent } = useAgent() + const invitationId = '1' + agent!.oob.findByReceivedInvitationId = jest.fn().mockReturnValueOnce(Promise.resolve({ id: '123' })) + const deleteById = jest.fn() + agent!.oob.deleteById = deleteById + + await removeExistingInvitationIfRequired(agent, invitationId) + + expect(deleteById).toBeCalledWith('123') + }) +}) + +describe('connectFromInvitation', () => { + test('ordinary connection, default options', async () => { + const { agent } = useAgent() + const uri = '' + const parseInvitation = jest.fn().mockReturnValueOnce(Promise.resolve({ id: '123' })) + agent!.oob.parseInvitation = parseInvitation + const record = {} + const receiveInvitation = jest.fn().mockReturnValueOnce(Promise.resolve(record)) + agent!.oob.receiveInvitation = receiveInvitation + const result = await connectFromInvitation(uri, agent) + expect(parseInvitation).toBeCalled() + expect(receiveInvitation).toBeCalled() + expect(result).toBe(record) + }) +}) + describe('getConnectionName', () => { test('With all properties and alternate name', async () => { const connection = { id: '1', theirLabel: 'Mike', alias: 'Mikey' }