diff --git a/.github/workflows/utils-tests.yml b/.github/workflows/utils-tests.yml new file mode 100644 index 0000000000..15edc17f60 --- /dev/null +++ b/.github/workflows/utils-tests.yml @@ -0,0 +1,29 @@ +name: Common package tests + +on: + pull_request: + paths: + - packages/common/** + +jobs: + utils-tests: + timeout-minutes: 25 + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-20.04, macos-latest, windows-2019] + + steps: + - name: "Print OS" + run: echo ${{ matrix.os }} + + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: "Setup environment" + uses: ./.github/actions/setup-env + with: + bootstrap-packages: "@quiet/eslint-config,@quiet/logger,@quiet/types,@quiet/common" + + - name: "Unit tests" + run: lerna run test --scope @quiet/common --stream diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a8e7bb4c..56600bb044 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ # New features: +* Add support for new format of invitation link: `c=&t=&s=&i=` ([#2310](https://github.com/TryQuiet/quiet/issues/2310)) + # Refactorings: -# Fixes: +# Fixes # Chores diff --git a/package.json b/package.json index 5c2e28ce5c..c9e44e9828 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "start:desktop": "lerna run --scope @quiet/desktop start", "lint:all": "lerna run lint", "distAndRunE2ETests:mac:local": "lerna run --scope @quiet/desktop distMac:local && lerna run --scope e2e-tests test:localBinary --", + "e2e:linux:build": "lerna run --scope @quiet/backend webpack:prod && lerna run --scope @quiet/desktop distUbuntu && lerna run --scope e2e-tests linux:copy", + "e2e:linux:run": "lerna run --scope e2e-tests test --", "prepare": "husky", "lint-staged": "lerna run lint-staged" }, diff --git a/packages/backend/jestSetup.js b/packages/backend/jestSetup.js index 269624f2bd..b82077ab46 100644 --- a/packages/backend/jestSetup.js +++ b/packages/backend/jestSetup.js @@ -1,11 +1,14 @@ -import { setEngine, CryptoEngine } from'pkijs' +import { setEngine, CryptoEngine } from 'pkijs' import { Crypto } from '@peculiar/webcrypto' -const crypto = new Crypto(); -global.crypto = crypto; +const crypto = new Crypto() +global.crypto = crypto -setEngine('newEngine', new CryptoEngine({ +setEngine( + 'newEngine', + new CryptoEngine({ name: 'newEngine', // @ts-ignore crypto: crypto, - })) + }) +) diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts index 98d717762b..5753e3feb0 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts @@ -5,8 +5,7 @@ import { CustomEvent } from '@libp2p/interfaces/events' import { jest, beforeEach, describe, it, expect, afterEach } from '@jest/globals' import { communities, getFactory, identity, prepareStore, Store } from '@quiet/state-manager' import { createPeerId, createTmpDir, libp2pInstanceParams, removeFilesFromDir, tmpQuietDirPath } from '../common/utils' - -import { NetworkStats, type Community, type Identity, type InitCommunityPayload } from '@quiet/types' +import { NetworkStats, type Community, type Identity } from '@quiet/types' import { LazyModuleLoader } from '@nestjs/core' import { TestingModule, Test } from '@nestjs/testing' import { FactoryGirl } from 'factory-girl' @@ -114,7 +113,6 @@ beforeEach(async () => { }) afterEach(async () => { - await libp2pService?.libp2pInstance?.stop() if (connectionsManagerService) { await connectionsManagerService.closeAllServices() } @@ -123,6 +121,10 @@ afterEach(async () => { describe('Connections manager', () => { it('saves peer stats when peer has been disconnected', async () => { + // @ts-expect-error + libp2pService.processInChunksService.init = jest.fn() + // @ts-expect-error + libp2pService.processInChunksService.process = jest.fn() class RemotePeerEventDetail { peerId: string @@ -138,6 +140,10 @@ describe('Connections manager', () => { // Peer connected await connectionsManagerService.init() + await connectionsManagerService.launchCommunity({ + community, + network: { peerId: userIdentity.peerId, hiddenService: userIdentity.hiddenService }, + }) libp2pService.connectedPeers.set(peerId.toString(), DateTime.utc().valueOf()) // Peer disconnected @@ -146,11 +152,16 @@ describe('Connections manager', () => { remotePeer: new RemotePeerEventDetail(peerId.toString()), remoteAddr: new RemotePeerEventDetail(remoteAddr), } + await waitForExpect(async () => { + expect(libp2pService.libp2pInstance).not.toBeUndefined() + }, 2_000) libp2pService.libp2pInstance?.dispatchEvent( new CustomEvent('peer:disconnect', { detail: peerDisconectEventDetail }) ) + await waitForExpect(async () => { + expect(libp2pService.connectedPeers.size).toEqual(0) + }, 2000) - expect(libp2pService.connectedPeers.size).toEqual(0) await waitForExpect(async () => { expect(await localDbService.get(LocalDBKeys.PEERS)).not.toBeNull() }, 2000) diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index d126eb3be1..77364128dd 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -21,6 +21,8 @@ import { type DeleteChannelResponse, type MessagesLoadedPayload, type NetworkInfo, + CreateNetworkPayload, + CommunityOwnership, } from '@quiet/types' import EventEmitter from 'events' import { CONFIG_OPTIONS, SERVER_IO_PROVIDER } from '../const' @@ -169,6 +171,21 @@ export class SocketService extends EventEmitter implements OnModuleInit { } ) + socket.on( + SocketActionTypes.DOWNLOAD_INVITE_DATA, + async (payload: { serverAddress: string; cid: string }, callback: (response: CreateNetworkPayload) => void) => { + // this.emit(SocketActionTypes.DOWNLOAD_INVITE_DATA, payload, callback) + console.log('download invite data', payload) + // Mock it for now + callback({ + ownership: CommunityOwnership.User, + peers: [], + psk: '', + ownerOrbitDbIdentity: '', + }) + } + ) + socket.on(SocketActionTypes.LEAVE_COMMUNITY, async () => { this.logger('Leaving community') this.emit(SocketActionTypes.LEAVE_COMMUNITY) diff --git a/packages/mobile/src/utils/functions/areObjectsEqual/areObjectsEqual.ts b/packages/common/src/compare.ts similarity index 58% rename from packages/mobile/src/utils/functions/areObjectsEqual/areObjectsEqual.ts rename to packages/common/src/compare.ts index d2f9899e4f..10db5fdbc0 100644 --- a/packages/mobile/src/utils/functions/areObjectsEqual/areObjectsEqual.ts +++ b/packages/common/src/compare.ts @@ -1,3 +1,4 @@ export const areObjectsEqual = (obj1: any, obj2: any): boolean => { + // Using this only makes sense for small objects whose properties are in the same order return JSON.stringify(obj1) === JSON.stringify(obj2) } diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 81cc0eb155..90c610f716 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -11,4 +11,5 @@ export * from './libp2p' export * from './tests' export * from './auth' export * from './messages' +export * from './compare' export * from './dir' diff --git a/packages/common/src/invitationCode.test.ts b/packages/common/src/invitationCode.test.ts index 5f18ae4378..6a48b409a2 100644 --- a/packages/common/src/invitationCode.test.ts +++ b/packages/common/src/invitationCode.test.ts @@ -1,120 +1,172 @@ -import { InvitationData } from '@quiet/types' +import { InvitationDataV1, InvitationDataVersion, InvitationPair } from '@quiet/types' import { argvInvitationCode, composeInvitationDeepUrl, - invitationShareUrl, composeInvitationShareUrl, parseInvitationCodeDeepUrl, PSK_PARAM_KEY, + OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, + p2pAddressesToPairs, + CID_PARAM_KEY, + TOKEN_PARAM_KEY, + SERVER_ADDRESS_PARAM_KEY, + INVITER_ADDRESS_PARAM_KEY, + DEEP_URL_SCHEME_WITH_SEPARATOR, } from './invitationCode' import { QUIET_JOIN_PAGE } from './static' +import { validInvitationDatav1, validInvitationDatav2 } from './tests' +import { createLibp2pAddress } from './libp2p' -describe('Invitation code helper', () => { - const peerId1 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA' - const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' - const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' - const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const psk = 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw%3D' - const pskDecoded = 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw=' - const ownerOrbitDbIdentity = 'testOwnerOrbitDbIdentity' +describe(`Invitation code helper ${InvitationDataVersion.v1}`, () => { + const address = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' + const peerId = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' + const data: InvitationDataV1 = { + ...validInvitationDatav1[0], + pairs: [...validInvitationDatav1[0].pairs, { peerId: peerId, onionAddress: address }], + } + const urlParams = [ + [data.pairs[0].peerId, data.pairs[0].onionAddress], + [data.pairs[1].peerId, data.pairs[1].onionAddress], + [PSK_PARAM_KEY, data.psk], + [OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity], + ] it('retrieves invitation code from argv', () => { - const expectedCodes: InvitationData = { - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk: pskDecoded, - ownerOrbitDbIdentity, - } - const result = argvInvitationCode([ - 'something', - 'quiet:/invalid', - 'zbay://invalid', - 'quiet://invalid', - 'quiet://?param=invalid', - composeInvitationDeepUrl(expectedCodes), - ]) - expect(result).toEqual(expectedCodes) + const result = argvInvitationCode(['something', 'quiet:/invalid', 'zbay://invalid', composeInvitationDeepUrl(data)]) + expect(result).toEqual(data) }) - it('returns null if argv do not contain any valid invitation code', () => { - const result = argvInvitationCode([ - 'something', - 'quiet:/invalid', - 'zbay://invalid', - 'quiet://invalid', - 'quiet://?param=invalid', - ]) + it('returns null if argv do not contain any url with proper scheme', () => { + const result = argvInvitationCode(['something', 'quiet:/invalid', 'zbay://invalid']) expect(result).toBeNull() }) + it('throws error if argv contains invalid invitation url', () => { + expect(() => { + argvInvitationCode(['something', 'quiet:/invalid', 'quiet://?param=invalid']) + }).toThrow() + }) + it('composes proper invitation deep url', () => { - expect( - composeInvitationDeepUrl({ - pairs: [ - { peerId: 'peerID1', onionAddress: 'address1' }, - { peerId: 'peerID2', onionAddress: 'address2' }, - ], - psk: pskDecoded, - ownerOrbitDbIdentity, - }) - ).toEqual(`quiet://?peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}`) + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + expect(composeInvitationDeepUrl(data)).toEqual(url.href) }) it('creates invitation share url based on invitation data', () => { - const pairs: InvitationData = { - pairs: [ - { peerId: 'peerID1', onionAddress: 'address1' }, - { peerId: 'peerID2', onionAddress: 'address2' }, - ], - psk: pskDecoded, - ownerOrbitDbIdentity, - } - const expected = `${QUIET_JOIN_PAGE}#peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}` - expect(composeInvitationShareUrl(pairs)).toEqual(expected) + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + expect(composeInvitationShareUrl(data)).toEqual(url.href.replace('?', '#')) }) - it('builds proper invitation share url from peers addresses', () => { + it('converts list of p2p addresses to invitation pairs', () => { + const pair: InvitationPair = { + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + onionAddress: 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad', + } const peerList = [ - '/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + createLibp2pAddress(pair.onionAddress, pair.peerId), 'invalidAddress', - '/dns4/somethingElse.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA', + createLibp2pAddress('somethingElse.onion', 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA'), ] - expect(invitationShareUrl(peerList, pskDecoded, ownerOrbitDbIdentity)).toEqual( - `${QUIET_JOIN_PAGE}#QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad&QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA=somethingElse&${PSK_PARAM_KEY}=${psk}` - ) + expect(p2pAddressesToPairs(peerList)).toEqual([pair]) }) it('retrieves invitation codes from deep url', () => { - const codes = parseInvitationCodeDeepUrl( - `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}` - ) + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + + const codes = parseInvitationCodeDeepUrl(url.href) expect(codes).toEqual({ - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk: pskDecoded, - ownerOrbitDbIdentity, + version: InvitationDataVersion.v1, + ...data, }) }) - it.each([['12345'], ['a2FzemE='], 'a2FycGllIHcgZ2FsYXJlY2llIGVjaWUgcGVjaWUgYWxlIGkgdGFrIHpqZWNpZQ=='])( - 'parsing invitation code throws error if psk is invalid: (%s)', - (psk: string) => { - expect(() => { - parseInvitationCodeDeepUrl(`quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}`) - }).toThrow() - } - ) - - it('retrieves invitation codes from deep url with partly invalid codes', () => { - const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLs' - const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const parsed = parseInvitationCodeDeepUrl( - `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}` - ) - expect(parsed).toEqual({ pairs: [{ peerId: peerId1, onionAddress: address1 }], psk: pskDecoded }) + it.each([ + [PSK_PARAM_KEY, '12345'], + [PSK_PARAM_KEY, 'a2FzemE='], + [PSK_PARAM_KEY, 'a2FycGllIHcgZ2FsYXJlY2llIGVjaWUgcGVjaWUgYWxlIGkgdGFrIHpqZWNpZQ=='], + ])('parsing deep url throws error if data is invalid: %s=%s', (paramKey: string, paramValue: string) => { + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + + // Replace valid param value with invalid one + url.searchParams.set(paramKey, paramValue) + + expect(() => { + parseInvitationCodeDeepUrl(url.href) + }).toThrow() + }) + + it('retrieves invitation codes from deep url with partly invalid addresses', () => { + const urlParamsWithInvalidAddress = [ + [data.pairs[0].peerId, data.pairs[0].onionAddress], + [data.pairs[1].peerId, data.pairs[1].onionAddress], + ['QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wf', 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdv'], + [PSK_PARAM_KEY, data.psk], + [OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity], + ] + + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParamsWithInvalidAddress.forEach(([key, value]) => url.searchParams.append(key, value)) + + const parsed = parseInvitationCodeDeepUrl(url.href) + expect(parsed).toEqual({ + version: InvitationDataVersion.v1, + ...data, + }) + }) +}) + +describe(`Invitation code helper ${InvitationDataVersion.v2}`, () => { + const data = validInvitationDatav2[0] + const urlParams = [ + [CID_PARAM_KEY, data.cid], + [TOKEN_PARAM_KEY, data.token], + [SERVER_ADDRESS_PARAM_KEY, data.serverAddress], + [INVITER_ADDRESS_PARAM_KEY, data.inviterAddress], + ] + + it('creates invitation share url based on invitation data', () => { + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + expect(composeInvitationShareUrl(data)).toEqual(url.href.replace('?', '#')) + }) + + it('composes proper invitation deep url', () => { + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + expect(composeInvitationDeepUrl(data)).toEqual(url.href) + }) + + it('retrieves invitation codes from deep url v2', () => { + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + const codes = parseInvitationCodeDeepUrl(url.href) + expect(codes).toEqual({ + version: InvitationDataVersion.v2, + cid: data.cid, + token: data.token, + serverAddress: data.serverAddress, + inviterAddress: data.inviterAddress, + }) + }) + + it.each([ + // TODO: add check for invalid token + [CID_PARAM_KEY, 'sth'], + [SERVER_ADDRESS_PARAM_KEY, 'website.com'], + [INVITER_ADDRESS_PARAM_KEY, 'abcd'], + ])('parsing deep url throws error if data is invalid: %s=%s', (paramKey: string, paramValue: string) => { + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + + // Replace valid param value with invalid one + url.searchParams.set(paramKey, paramValue) + + expect(() => { + parseInvitationCodeDeepUrl(url.href) + }).toThrow() }) }) diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index 470fb6536e..16182ed325 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -1,12 +1,21 @@ -import { InvitationData, InvitationPair } from '@quiet/types' +import { InvitationData, InvitationDataV1, InvitationDataV2, InvitationDataVersion, InvitationPair } from '@quiet/types' import { QUIET_JOIN_PAGE } from './static' import { createLibp2pAddress, isPSKcodeValid } from './libp2p' +// import { CID } from 'multiformats/cid' // Fixme: dependency issue import Logger from './logger' const logger = Logger('invite') +// V1 invitation code format (p2p without relay) export const PSK_PARAM_KEY = 'k' export const OWNER_ORBIT_DB_IDENTITY_PARAM_KEY = 'o' -const DEEP_URL_SCHEME_WITH_SEPARATOR = 'quiet://' + +// V2 invitation code format (relay support) +export const CID_PARAM_KEY = 'c' +export const TOKEN_PARAM_KEY = 't' +export const INVITER_ADDRESS_PARAM_KEY = 'i' +export const SERVER_ADDRESS_PARAM_KEY = 's' + +export const DEEP_URL_SCHEME_WITH_SEPARATOR = 'quiet://' const DEEP_URL_SCHEME = 'quiet' const ONION_ADDRESS_REGEX = /^[a-z0-9]{56}$/g const PEER_ID_REGEX = /^[a-zA-Z0-9]{46}$/g @@ -16,6 +25,53 @@ interface ParseDeepUrlParams { expectedProtocol?: string } +const parseCodeV2 = (url: string): InvitationDataV2 => { + /** + * c=&t=&s=&i= + */ + const params = new URL(url).searchParams + const requiredParams = [CID_PARAM_KEY, TOKEN_PARAM_KEY, SERVER_ADDRESS_PARAM_KEY, INVITER_ADDRESS_PARAM_KEY] + + const entries = validateUrlParams(params, requiredParams) + + return { + version: InvitationDataVersion.v2, + cid: entries[CID_PARAM_KEY], + token: entries[TOKEN_PARAM_KEY], + serverAddress: entries[SERVER_ADDRESS_PARAM_KEY], + inviterAddress: entries[INVITER_ADDRESS_PARAM_KEY], + } +} + +const parseCodeV1 = (url: string): InvitationDataV1 => { + /** + * =&=...&k=&o= + */ + const params = new URL(url).searchParams + const requiredParams = [PSK_PARAM_KEY, OWNER_ORBIT_DB_IDENTITY_PARAM_KEY] + + const entries = validateUrlParams(params, requiredParams) + + const codes: InvitationPair[] = [] + + params.forEach((onionAddress, peerId) => { + if (!peerDataValid({ peerId, onionAddress })) return + codes.push({ + peerId, + onionAddress, + }) + }) + + if (codes.length === 0) throw new Error(`No valid peer addresses found in invitation code '${url}'`) + + return { + version: InvitationDataVersion.v1, + pairs: codes, + psk: entries[PSK_PARAM_KEY], + ownerOrbitDbIdentity: entries[OWNER_ORBIT_DB_IDENTITY_PARAM_KEY], + } +} + const parseDeepUrl = ({ url, expectedProtocol = `${DEEP_URL_SCHEME}:` }: ParseDeepUrlParams): InvitationData => { let _url = url let validUrl: URL | null = null @@ -23,7 +79,7 @@ const parseDeepUrl = ({ url, expectedProtocol = `${DEEP_URL_SCHEME}:` }: ParseDe if (!expectedProtocol) { // Create a full url to be able to use the same URL parsing mechanism expectedProtocol = `${DEEP_URL_SCHEME}:` - _url = `${DEEP_URL_SCHEME}://?${url}` + _url = `${DEEP_URL_SCHEME_WITH_SEPARATOR}?${url}` } try { @@ -38,32 +94,22 @@ const parseDeepUrl = ({ url, expectedProtocol = `${DEEP_URL_SCHEME}:` }: ParseDe } const params = validUrl.searchParams - const codes: InvitationPair[] = [] - let psk = params.get(PSK_PARAM_KEY) - if (!psk) throw new Error(`No psk found in invitation code '${url}'`) - psk = decodeURIComponent(psk) - if (!isPSKcodeValid(psk)) throw new Error(`Invalid psk in invitation code '${url}'`) - params.delete(PSK_PARAM_KEY) + const psk = params.get(PSK_PARAM_KEY) + const cid = params.get(CID_PARAM_KEY) + if (!psk && !cid) throw new Error(`Invitation code does not match either v1 or v2 format '${url}'`) - let ownerOrbitDbIdentity = params.get(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY) - if (!ownerOrbitDbIdentity) throw new Error(`No owner OrbitDB identity found in invitation code '${url}'`) - ownerOrbitDbIdentity = decodeURIComponent(ownerOrbitDbIdentity) - params.delete(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY) - - params.forEach((onionAddress, peerId) => { - if (!peerDataValid({ peerId, onionAddress })) return - codes.push({ - peerId, - onionAddress, - }) - }) - logger('Retrieved data:', codes) - return { - pairs: codes, - psk, - ownerOrbitDbIdentity, + let data: InvitationData + if (psk) { + data = parseCodeV1(_url) + } else { + data = parseCodeV2(_url) } + + if (!data) throw new Error(`Could not parse invitation code from deep url '${url}'`) + + logger(`Invitation data '${data}' parsed`) + return data } /** @@ -81,15 +127,12 @@ export const parseInvitationCode = (code: string): InvitationData => { return parseDeepUrl({ url: code, expectedProtocol: '' }) } -/** - * @arg {string[]} peers - List of peer's p2p addresses - * @arg psk - Pre shared key in base64 - * @returns {string} - Complete shareable invitation link, e.g. - * https://tryquiet.org/join/#=&=&k=&o= - */ -export const invitationShareUrl = (peers: string[] = [], psk: string, ownerOrbitDbIdentity: string): string => { +export const p2pAddressesToPairs = (addresses: string[]): InvitationPair[] => { + /** + * @arg {string[]} addresses - List of peer's p2p addresses + */ const pairs: InvitationPair[] = [] - for (const peerAddress of peers) { + for (const peerAddress of addresses) { let peerId: string let onionAddress: string try { @@ -110,10 +153,11 @@ export const invitationShareUrl = (peers: string[] = [], psk: string, ownerOrbit continue } const rawAddress = onionAddress.endsWith('.onion') ? onionAddress.split('.')[0] : onionAddress + if (!peerDataValid({ peerId, onionAddress: rawAddress })) continue + pairs.push({ peerId: peerId, onionAddress: rawAddress }) } - - return composeInvitationShareUrl({ pairs, psk, ownerOrbitDbIdentity }) + return pairs } export const pairsToP2pAddresses = (pairs: InvitationPair[]): string[] => { @@ -125,6 +169,10 @@ export const pairsToP2pAddresses = (pairs: InvitationPair[]): string[] => { } export const composeInvitationShareUrl = (data: InvitationData) => { + /** + * @returns {string} - Complete shareable invitation link, e.g. + * https://tryquiet.org/join/#=&=&k=&o= + */ return composeInvitationUrl(`${QUIET_JOIN_PAGE}`, data).replace('?', '#') } @@ -132,13 +180,26 @@ export const composeInvitationDeepUrl = (data: InvitationData): string => { return composeInvitationUrl(`${DEEP_URL_SCHEME_WITH_SEPARATOR}`, data) } -const composeInvitationUrl = (baseUrl: string, data: InvitationData): string => { +const composeInvitationUrl = (baseUrl: string, data: InvitationDataV1 | InvitationDataV2): string => { const url = new URL(baseUrl) - for (const pair of data.pairs) { - url.searchParams.append(pair.peerId, pair.onionAddress) + + if (!data.version) data.version = InvitationDataVersion.v1 + + switch (data.version) { + case InvitationDataVersion.v1: + for (const pair of data.pairs) { + url.searchParams.append(pair.peerId, pair.onionAddress) + } + url.searchParams.append(PSK_PARAM_KEY, data.psk) + url.searchParams.append(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity) + break + case InvitationDataVersion.v2: + url.searchParams.append(CID_PARAM_KEY, data.cid) + url.searchParams.append(TOKEN_PARAM_KEY, data.token) + url.searchParams.append(SERVER_ADDRESS_PARAM_KEY, data.serverAddress) + url.searchParams.append(INVITER_ADDRESS_PARAM_KEY, data.inviterAddress) + break } - url.searchParams.append(PSK_PARAM_KEY, data.psk) - url.searchParams.append(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity) return url.href } @@ -148,15 +209,19 @@ const composeInvitationUrl = (baseUrl: string, data: InvitationData): string => export const argvInvitationCode = (argv: string[]): InvitationData | null => { let invitationData: InvitationData | null = null for (const arg of argv) { - try { - invitationData = parseInvitationCodeDeepUrl(arg) - } catch (e) { + if (!arg.startsWith(DEEP_URL_SCHEME_WITH_SEPARATOR)) { + console.log('Not a deep url, not parsing', arg) continue } - if (invitationData.pairs.length > 0) { - break - } else { - invitationData = null + console.log('Parsing deep url', arg) + invitationData = parseInvitationCodeDeepUrl(arg) + switch (invitationData.version) { + case InvitationDataVersion.v1: + if (invitationData.pairs.length > 0) { + break + } else { + invitationData = null + } } } return invitationData @@ -174,3 +239,60 @@ const peerDataValid = ({ peerId, onionAddress }: { peerId: string; onionAddress: } return true } + +const validateUrlParams = (params: URLSearchParams, requiredParams: string[]) => { + const entries = Object.fromEntries(params) + + requiredParams.forEach(key => { + const value = params.get(key) + if (!value) { + throw new Error(`Missing key '${key}' in invitation code`) + } + entries[key] = decodeURIComponent(value) + if (!isParamValid(key, entries[key])) { + throw new Error(`Invalid value '${value}' for key '${key}' in invitation code`) + } + params.delete(key) + }) + return entries +} + +const isParamValid = (param: string, value: string) => { + logger(`Validating param ${param} with value ${value}`) + switch (param) { + case CID_PARAM_KEY: + // try { + // CID.parse(value) + // } catch (e) { + // logger.error(e.message) + // return false + // } + return Boolean(value.match(PEER_ID_REGEX)) + + case TOKEN_PARAM_KEY: + // TODO: validate token format + return true + + case SERVER_ADDRESS_PARAM_KEY: + try { + new URL(value) + } catch (e) { + logger.error(e.message) + return false + } + return true + + case INVITER_ADDRESS_PARAM_KEY: + return Boolean(value.trim().match(ONION_ADDRESS_REGEX)) + + case PSK_PARAM_KEY: + return isPSKcodeValid(value) + + case OWNER_ORBIT_DB_IDENTITY_PARAM_KEY: + // TODO: validate orbit db identity format? + return true + + default: + return false + } +} diff --git a/packages/common/src/tests.ts b/packages/common/src/tests.ts index 92acc606cf..6c4791f4bb 100644 --- a/packages/common/src/tests.ts +++ b/packages/common/src/tests.ts @@ -1,8 +1,8 @@ -import { InvitationData } from '@quiet/types' +import { InvitationData, InvitationDataV1, InvitationDataV2, InvitationDataVersion } from '@quiet/types' import { composeInvitationDeepUrl, composeInvitationShareUrl } from './invitationCode' import { QUIET_JOIN_PAGE } from './static' -export const validInvitationCodeTestData: InvitationData[] = [ +export const validInvitationDatav1: InvitationDataV1[] = [ { pairs: [ { @@ -25,7 +25,26 @@ export const validInvitationCodeTestData: InvitationData[] = [ }, ] -export const getValidInvitationUrlTestData = (data: InvitationData) => { +export const validInvitationDatav2: InvitationDataV2[] = [ + { + version: InvitationDataVersion.v2, + cid: 'QmaRchXhkPWq8iLiMZwFfd2Yi4iESWhAYYJt8cTCVXSwpG', + token: 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw', + serverAddress: 'https://tryquiet.org/api/', + inviterAddress: 'pgzlcstu4ljvma7jqyalimcxlvss5bwlbba3c3iszgtwxee4qjdlgeqd', + }, +] + +export const validInvitationCodeTestData: InvitationData[] = [...validInvitationDatav1] + +type TestData = { + shareUrl: () => string + deepUrl: () => string + code: () => string + data: T +} + +export function getValidInvitationUrlTestData(data: T): TestData { return { shareUrl: () => composeInvitationShareUrl(data), deepUrl: () => composeInvitationDeepUrl(data), @@ -33,3 +52,12 @@ export const getValidInvitationUrlTestData = (data: InvitationData) => { data: data, } } + +// export const getValidInvitationUrlTestData = (data: InvitationData) => { +// return { +// shareUrl: () => composeInvitationShareUrl(data), +// deepUrl: () => composeInvitationDeepUrl(data), +// code: () => composeInvitationShareUrl(data).split(QUIET_JOIN_PAGE + '#')[1], +// data: data, +// } +// } diff --git a/packages/desktop/src/main/invitation.ts b/packages/desktop/src/main/invitation.ts index 2616059683..cfc1bf429a 100644 --- a/packages/desktop/src/main/invitation.ts +++ b/packages/desktop/src/main/invitation.ts @@ -3,15 +3,12 @@ import path from 'path' import os from 'os' import { execSync } from 'child_process' import { BrowserWindow } from 'electron' -import { InvitationData, InvitationPair } from '@quiet/types' -export const processInvitationCode = (mainWindow: BrowserWindow, data: InvitationData | null) => { - if (!data || data?.pairs.length === 0) { - console.log('No valid invitation codes, not processing') - return - } +export const processInvitationCode = (mainWindow: BrowserWindow, code: string | string[]) => { + console.log('processInvitationCode:', code) + if (!code || !code.length) return mainWindow.webContents.send('invitation', { - data, + code, }) } diff --git a/packages/desktop/src/main/main.test.ts b/packages/desktop/src/main/main.test.ts index dca57a360c..66d2873457 100644 --- a/packages/desktop/src/main/main.test.ts +++ b/packages/desktop/src/main/main.test.ts @@ -250,27 +250,28 @@ describe('Invitation code', () => { expect(mockAppOnCalls[1][0]).toBe('open-url') const event = { preventDefault: () => {} } - mockAppOnCalls[1][1](event, composeInvitationDeepUrl(codes)) - expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { data: codes }) + const deepUrl = composeInvitationDeepUrl(codes) + mockAppOnCalls[1][1](event, deepUrl) + expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { code: deepUrl }) }) - it('do not process invitation code on open-url event (on macos) if url is invalid', async () => { - codes['psk'] = '12345' + it('do not process invitation code on open-url event (on macos) if url is empty', async () => { expect(mockAppOnCalls[2][0]).toBe('ready') await mockAppOnCalls[2][1]() expect(mockAppOnCalls[1][0]).toBe('open-url') const event = { preventDefault: () => {} } - mockAppOnCalls[1][1](event, composeInvitationDeepUrl(codes)) - expect(mockWindowWebContentsSend).not.toHaveBeenCalledWith('invitation', { data: codes }) + mockAppOnCalls[1][1](event, '') + expect(mockWindowWebContentsSend).not.toHaveBeenCalledWith('invitation', { code: '' }) }) it('process invitation code on second-instance event', async () => { await mockAppOnCalls[2][1]() - const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', composeInvitationDeepUrl(codes)] + const deepUrl = composeInvitationDeepUrl(codes) + const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', deepUrl, 'something/else'] expect(mockAppOnCalls[0][0]).toBe('second-instance') const event = { preventDefault: () => {} } mockAppOnCalls[0][1](event, commandLine) - expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { data: codes }) + expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { code: deepUrl }) }) }) diff --git a/packages/desktop/src/main/main.ts b/packages/desktop/src/main/main.ts index b1b65199b4..7b0c4b96be 100644 --- a/packages/desktop/src/main/main.ts +++ b/packages/desktop/src/main/main.ts @@ -79,8 +79,7 @@ if (!gotTheLock) { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore() mainWindow.focus() - const invitationCode = argvInvitationCode(commandLine) - processInvitationCode(mainWindow, invitationCode) + processInvitationCode(mainWindow, commandLine) } }) } @@ -156,12 +155,7 @@ app.on('open-url', (event, url) => { event.preventDefault() if (mainWindow) { invitationUrl = null - try { - const invitationData = parseInvitationCodeDeepUrl(url) - processInvitationCode(mainWindow, invitationData) - } catch (e) { - console.warn(e.message) - } + processInvitationCode(mainWindow, url) } }) @@ -494,8 +488,7 @@ app.on('ready', async () => { } if (process.platform === 'darwin' && invitationUrl) { try { - const invitationData = parseInvitationCodeDeepUrl(invitationUrl) - processInvitationCode(mainWindow, invitationData) + processInvitationCode(mainWindow, invitationUrl) } catch (e) { console.warn(e.message) } finally { @@ -504,8 +497,7 @@ app.on('ready', async () => { } if (process.platform !== 'darwin' && process.argv) { try { - const invitationCode = argvInvitationCode(process.argv) - processInvitationCode(mainWindow, invitationCode) + processInvitationCode(mainWindow, process.argv) } catch (e) { console.warn(e.message) } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx index ee51e711e2..6d3d270fc8 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx @@ -23,10 +23,11 @@ import { validInvitationCodeTestData, getValidInvitationUrlTestData, PSK_PARAM_KEY, + validInvitationDatav1, } from '@quiet/common' describe('join community', () => { - const { code, data } = getValidInvitationUrlTestData(validInvitationCodeTestData[0]) + const { code, data } = getValidInvitationUrlTestData(validInvitationDatav1[0]) const validCode = code() diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx index d4a63352ab..0d03c60f01 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx @@ -1,11 +1,11 @@ +import { communities, connection, identity } from '@quiet/state-manager' +import { CommunityOwnership, InvitationData } from '@quiet/types' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { socketSelectors } from '../../../sagas/socket/socket.selectors' -import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationPair } from '@quiet/types' -import { communities, identity, connection, network } from '@quiet/state-manager' import PerformCommunityActionComponent from '../../../components/CreateJoinCommunity/PerformCommunityActionComponent' -import { ModalName } from '../../../sagas/modals/modals.types' import { useModal } from '../../../containers/hooks' +import { ModalName } from '../../../sagas/modals/modals.types' +import { socketSelectors } from '../../../sagas/socket/socket.selectors' const JoinCommunity = () => { const dispatch = useDispatch() @@ -15,10 +15,6 @@ const JoinCommunity = () => { const currentCommunity = useSelector(communities.selectors.currentCommunity) const currentIdentity = useSelector(identity.selectors.currentIdentity) - // Invitation link data should be already available if user joined via deep link - const invitationCodes = useSelector(communities.selectors.invitationCodes) - const psk = useSelector(communities.selectors.psk) - const joinCommunityModal = useModal(ModalName.joinCommunityModal) const createCommunityModal = useModal(ModalName.createCommunityModal) @@ -39,13 +35,7 @@ const JoinCommunity = () => { }, [currentCommunity]) const handleCommunityAction = (data: InvitationData) => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: data.pairs, - psk: data.psk, - ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, - } - dispatch(communities.actions.createNetwork(payload)) + dispatch(communities.actions.joinNetwork(data)) } // From 'You can create a new community instead' link @@ -72,8 +62,6 @@ const JoinCommunity = () => { hasReceivedResponse={Boolean(currentIdentity && !currentIdentity.userCertificate)} revealInputValue={revealInputValue} handleClickInputReveal={handleClickInputReveal} - invitationCode={invitationCodes} - psk={psk} /> ) } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx index 754bb453f4..5bff2b131f 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx @@ -137,9 +137,6 @@ export interface PerformCommunityActionProps { hasReceivedResponse: boolean revealInputValue?: boolean handleClickInputReveal?: () => void - invitationCode?: InvitationPair[] - psk?: string - ownerOrbitDbIdentity?: string } export const PerformCommunityActionComponent: React.FC = ({ @@ -153,9 +150,6 @@ export const PerformCommunityActionComponent: React.FC { const [formSent, setFormSent] = useState(false) @@ -219,18 +213,6 @@ export const PerformCommunityActionComponent: React.FC { - if (communityOwnership === CommunityOwnership.User && invitationCode?.length && psk && ownerOrbitDbIdentity) { - setFormSent(true) - setValue('name', composeInvitationShareUrl({ pairs: invitationCode, psk, ownerOrbitDbIdentity })) - } - }, [communityOwnership, invitationCode]) - useEffect(() => { if (!open) { setValue('name', '') diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index 3c478402b2..c73254eb47 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -20,10 +20,18 @@ ipcRenderer.on('force-save-state', async _event => { ipcRenderer.send('state-saved') }) -ipcRenderer.on('invitation', (_event, invitation: { data: InvitationData }) => { - if (!invitation.data) return - console.log('invitation', invitation.data.pairs, 'dispatching action') - store.dispatch(communities.actions.customProtocol(invitation.data)) +ipcRenderer.on('invitation', (_event, invitation: { code: string | string[] }) => { + console.log('ipcRenderer.on(invitation)', invitation) + if (!invitation.code) return + + let invitationData: string[] + if (typeof invitation.code === 'string') { + invitationData = [invitation.code] + } else { + invitationData = invitation.code + } + console.log('invitation', invitationData, 'dispatching action') + store.dispatch(communities.actions.customProtocol(invitationData)) }) ipcRenderer.on('socketIOSecret', (_event, socketIOSecret) => { diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts index ac732ee519..c97078e808 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts @@ -1,5 +1,5 @@ import { communities, getFactory, Store } from '@quiet/state-manager' -import { Community, CommunityOwnership, CreateNetworkPayload, InvitationData } from '@quiet/types' +import { Community, CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationDataV1 } from '@quiet/types' import { FactoryGirl } from 'factory-girl' import { expectSaga } from 'redux-saga-test-plan' import { customProtocolSaga } from './customProtocol.saga' @@ -8,13 +8,14 @@ import { prepareStore } from '../../testUtils/prepareStore' import { StoreKeys } from '../../store/store.keys' import { modalsActions } from '../modals/modals.slice' import { ModalName } from '../modals/modals.types' -import { validInvitationCodeTestData, getValidInvitationUrlTestData } from '@quiet/common' +import { getValidInvitationUrlTestData, validInvitationDatav1, validInvitationDatav2 } from '@quiet/common' describe('Handle invitation code', () => { let store: Store let factory: FactoryGirl let community: Community - let validInvitationData: InvitationData + let validInvitationData: InvitationDataV1 + let validInvitationDeepUrl: string beforeEach(async () => { store = ( @@ -28,19 +29,23 @@ describe('Handle invitation code', () => { factory = await getFactory(store) - validInvitationData = getValidInvitationUrlTestData(validInvitationCodeTestData[0]).data + validInvitationData = getValidInvitationUrlTestData(validInvitationDatav1[0]).data + validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationDatav1[0]).deepUrl() }) - it('creates network if code is valid', async () => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: validInvitationData.pairs, - psk: validInvitationData.psk, - ownerOrbitDbIdentity: validInvitationData.ownerOrbitDbIdentity, - } - await expectSaga(customProtocolSaga, communities.actions.customProtocol(validInvitationData)) + it('joins network if code is valid', async () => { + await expectSaga(customProtocolSaga, communities.actions.customProtocol([validInvitationDeepUrl])) + .withState(store.getState()) + .put(communities.actions.joinNetwork(validInvitationData)) + .run() + }) + + it('joins network if v2 code is valid', async () => { + const validInvitationData = getValidInvitationUrlTestData(validInvitationDatav2[0]).data + const validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationDatav2[0]).deepUrl() + await expectSaga(customProtocolSaga, communities.actions.customProtocol([validInvitationDeepUrl])) .withState(store.getState()) - .put(communities.actions.createNetwork(payload)) + .put(communities.actions.joinNetwork(validInvitationData)) .run() }) @@ -52,7 +57,7 @@ describe('Handle invitation code', () => { psk: validInvitationData.psk, } - await expectSaga(customProtocolSaga, communities.actions.customProtocol(validInvitationData)) + await expectSaga(customProtocolSaga, communities.actions.customProtocol([validInvitationDeepUrl])) .withState(store.getState()) .put( modalsActions.openModal({ @@ -67,7 +72,7 @@ describe('Handle invitation code', () => { .run() }) - it('does not try to create network if code is missing addresses', async () => { + it('does not try to create network if code is missing psk', async () => { const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, peers: [], @@ -75,11 +80,7 @@ describe('Handle invitation code', () => { await expectSaga( customProtocolSaga, - communities.actions.customProtocol({ - pairs: [], - psk: '12345', - ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', - }) + communities.actions.customProtocol(['someArg', 'quiet://?k=BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw=']) ) .withState(store.getState()) .put(communities.actions.clearInvitationCodes()) @@ -104,11 +105,9 @@ describe('Handle invitation code', () => { await expectSaga( customProtocolSaga, - communities.actions.customProtocol({ - pairs: validInvitationData.pairs, - psk: '', - ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', - }) + communities.actions.customProtocol([ + 'quiet://?QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + ]) ) .withState(store.getState()) .put(communities.actions.clearInvitationCodes()) diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts index 98a26e3035..0e889f7ed1 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts @@ -1,14 +1,20 @@ import { PayloadAction } from '@reduxjs/toolkit' import { select, put, delay } from 'typed-redux-saga' -import { CommunityOwnership, CreateNetworkPayload } from '@quiet/types' +import { InvitationData, InvitationDataVersion } from '@quiet/types' import { communities } from '@quiet/state-manager' import { socketSelectors } from '../socket/socket.selectors' import { ModalName } from '../modals/modals.types' import { modalsActions } from '../modals/modals.slice' +import { argvInvitationCode } from '@quiet/common' export function* customProtocolSaga( action: PayloadAction['payload']> ): Generator { + // TODO: refactor to remove code duplication. This is a slightly adjusted code from deepLink.saga.ts + const code = action.payload + + console.log('INIT_NAVIGATION: Waiting for websocket connection before proceeding with deep link flow.') + while (true) { const connected = yield* select(socketSelectors.isConnected) if (connected) { @@ -17,8 +23,56 @@ export function* customProtocolSaga( yield* delay(500) } + console.log('INIT_NAVIGATION: Continuing on deep link flow.') + + let data: InvitationData | null + + try { + data = argvInvitationCode(code) + } catch (e) { + console.warn(e.message) + + yield* put(communities.actions.clearInvitationCodes()) + yield* put( + modalsActions.openModal({ + name: ModalName.warningModal, + args: { + title: 'Invalid link', + subtitle: 'The invite link you received is not valid. Please check it and try again.', + }, + }) + ) + return + } + + if (data === null) { + console.log(`Not processing invitation code ${code}`) + return + } + const community = yield* select(communities.selectors.currentCommunity) - if (community) { + + let isJoiningAnotherCommunity = false + + switch (data.version) { + case InvitationDataVersion.v1: + const storedPsk = yield* select(communities.selectors.psk) + const currentPsk = data.psk + + console.log('Stored psk', storedPsk) + console.log('Current psk', currentPsk) + + isJoiningAnotherCommunity = Boolean(storedPsk && storedPsk !== currentPsk) + break + } + + const isAlreadyConnected = Boolean(community?.name) + const connectingWithAnotherCommunity = isJoiningAnotherCommunity && !isAlreadyConnected + + // User already belongs to a community + if (isAlreadyConnected) { + console.log('INIT_NAVIGATION: Displaying error (user already belongs to a community).') + yield* put(communities.actions.clearInvitationCodes()) // TODO: check out yield* put( modalsActions.openModal({ name: ModalName.warningModal, @@ -28,27 +82,25 @@ export function* customProtocolSaga( }, }) ) + return } - const invitationData = action.payload - if (invitationData && invitationData.pairs.length > 0 && invitationData.psk && invitationData.ownerOrbitDbIdentity) { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: invitationData.pairs, - psk: invitationData.psk, - ownerOrbitDbIdentity: invitationData.ownerOrbitDbIdentity, - } - yield* put(communities.actions.createNetwork(payload)) - } else { - yield* put(communities.actions.clearInvitationCodes()) + + if (connectingWithAnotherCommunity) { + console.log('INIT_NAVIGATION: Displaying error (user is already connecting to another community).') + yield* put(communities.actions.clearInvitationCodes()) // TODO: check out yield* put( modalsActions.openModal({ name: ModalName.warningModal, args: { - title: 'Invalid link', - subtitle: 'The invite link you received is not valid. Please check it and try again.', + title: 'You already started to connect to another community', + subtitle: "We're sorry but for now you can only be a member of a single community at a time.", }, }) ) + + return } + + yield* put(communities.actions.joinNetwork(data)) } diff --git a/packages/desktop/src/renderer/testUtils/index.ts b/packages/desktop/src/renderer/testUtils/index.ts index 2a8f54c14e..8138beb934 100644 --- a/packages/desktop/src/renderer/testUtils/index.ts +++ b/packages/desktop/src/renderer/testUtils/index.ts @@ -1,4 +1,3 @@ export * from './generateMessages' export * from './prepareStore' export * from './renderComponent' -export * from './socket' diff --git a/packages/desktop/src/rtl-tests/channel.add.test.tsx b/packages/desktop/src/rtl-tests/channel.add.test.tsx index 06534239b6..027949c65c 100644 --- a/packages/desktop/src/rtl-tests/channel.add.test.tsx +++ b/packages/desktop/src/rtl-tests/channel.add.test.tsx @@ -6,7 +6,7 @@ import { act } from 'react-dom/test-utils' import { take } from 'typed-redux-saga' import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' +import { socketEventData } from '@quiet/types' import { renderComponent } from '../renderer/testUtils/renderComponent' import { prepareStore } from '../renderer/testUtils/prepareStore' import { StoreKeys } from '../renderer/store/store.keys' diff --git a/packages/desktop/src/rtl-tests/channel.main.test.tsx b/packages/desktop/src/rtl-tests/channel.main.test.tsx index 924111e61b..c4148b6ac0 100644 --- a/packages/desktop/src/rtl-tests/channel.main.test.tsx +++ b/packages/desktop/src/rtl-tests/channel.main.test.tsx @@ -6,7 +6,7 @@ import { apply, take } from 'typed-redux-saga' import userEvent from '@testing-library/user-event' import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' +import { socketEventData } from '@quiet/types' import { renderComponent } from '../renderer/testUtils/renderComponent' import { prepareStore } from '../renderer/testUtils/prepareStore' import Channel from '../renderer/components/Channel/Channel' diff --git a/packages/desktop/src/rtl-tests/community.create.test.tsx b/packages/desktop/src/rtl-tests/community.create.test.tsx index 446409f0e2..d5f747cb65 100644 --- a/packages/desktop/src/rtl-tests/community.create.test.tsx +++ b/packages/desktop/src/rtl-tests/community.create.test.tsx @@ -13,7 +13,7 @@ import { ModalName } from '../renderer/sagas/modals/modals.types' import { CreateCommunityDictionary } from '../renderer/components/CreateJoinCommunity/community.dictionary' import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' +import { socketEventData } from '@quiet/types' import { Community, type InitCommunityPayload, diff --git a/packages/desktop/src/rtl-tests/community.join.test.tsx b/packages/desktop/src/rtl-tests/community.join.test.tsx index 0d6f4c1af9..6b7d0331ff 100644 --- a/packages/desktop/src/rtl-tests/community.join.test.tsx +++ b/packages/desktop/src/rtl-tests/community.join.test.tsx @@ -13,7 +13,7 @@ import { ModalName } from '../renderer/sagas/modals/modals.types' import { JoinCommunityDictionary } from '../renderer/components/CreateJoinCommunity/community.dictionary' import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' +import { socketEventData } from '@quiet/types' import { communities, RegisterUserCertificatePayload, @@ -169,6 +169,7 @@ describe('User', () => { expect(actions).toMatchInlineSnapshot(` Array [ + "Communities/joinNetwork", "Communities/createNetwork", "Communities/addNewCommunity", "Communities/setCurrentCommunity", diff --git a/packages/desktop/src/rtl-tests/customProtocol.test.tsx b/packages/desktop/src/rtl-tests/customProtocol.test.tsx index 33a3e363bf..3064efc590 100644 --- a/packages/desktop/src/rtl-tests/customProtocol.test.tsx +++ b/packages/desktop/src/rtl-tests/customProtocol.test.tsx @@ -11,6 +11,7 @@ import { ModalName } from '../renderer/sagas/modals/modals.types' import JoinCommunity from '../renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity' import CreateUsername from '../renderer/components/CreateUsername/CreateUsername' import { type Community, type InvitationData } from '@quiet/types' +import { composeInvitationDeepUrl } from '@quiet/common' jest.setTimeout(20_000) @@ -63,7 +64,7 @@ describe('Opening app through custom protocol', () => { ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', } - store.dispatch(communities.actions.customProtocol(invitationCodes)) + store.dispatch(communities.actions.customProtocol([composeInvitationDeepUrl(invitationCodes)])) store.dispatch(modalsActions.openModal({ name: ModalName.joinCommunityModal })) diff --git a/packages/desktop/src/rtl-tests/deep.linking.test.tsx b/packages/desktop/src/rtl-tests/deep.linking.test.tsx index 653029d1b1..983523353e 100644 --- a/packages/desktop/src/rtl-tests/deep.linking.test.tsx +++ b/packages/desktop/src/rtl-tests/deep.linking.test.tsx @@ -7,7 +7,7 @@ import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' import { prepareStore } from '../renderer/testUtils/prepareStore' import { renderComponent } from '../renderer/testUtils/renderComponent' -import { validInvitationCodeTestData } from '@quiet/common' +import { getValidInvitationUrlTestData, validInvitationCodeTestData } from '@quiet/common' import { communities } from '@quiet/state-manager' describe('Deep linking', () => { @@ -34,13 +34,17 @@ describe('Deep linking', () => { renderComponent(<>, store) - store.dispatch(communities.actions.customProtocol(validInvitationCodeTestData[0])) + store.dispatch( + communities.actions.customProtocol([getValidInvitationUrlTestData(validInvitationCodeTestData[0]).deepUrl()]) + ) await act(async () => {}) const originalPair = communities.selectors.invitationCodes(store.getState()) // Redo the action to provoke renewed saga runs - store.dispatch(communities.actions.customProtocol(validInvitationCodeTestData[1])) + store.dispatch( + communities.actions.customProtocol([getValidInvitationUrlTestData(validInvitationCodeTestData[1]).deepUrl()]) + ) await act(async () => {}) const currentPair = communities.selectors.invitationCodes(store.getState()) @@ -50,6 +54,7 @@ describe('Deep linking', () => { expect(actions).toMatchInlineSnapshot(` Array [ "Communities/customProtocol", + "Communities/joinNetwork", "Communities/createNetwork", "Communities/addNewCommunity", "Communities/setCurrentCommunity", diff --git a/packages/e2e-tests/.gitignore b/packages/e2e-tests/.gitignore index 3d928d694b..6ca1a6bae7 100644 --- a/packages/e2e-tests/.gitignore +++ b/packages/e2e-tests/.gitignore @@ -2,4 +2,5 @@ lib/ screenshots/ .DS_Store -Quiet/Quiet** \ No newline at end of file +Quiet/Quiet** +.env \ No newline at end of file diff --git a/packages/e2e-tests/package-lock.json b/packages/e2e-tests/package-lock.json index bbcfdb5f02..7e083ff08a 100644 --- a/packages/e2e-tests/package-lock.json +++ b/packages/e2e-tests/package-lock.json @@ -21,6 +21,7 @@ "@types/jest": "^29.2.6", "@types/selenium-webdriver": "^4.1.10", "babel-jest": "^29.3.1", + "dotenv": "16.4.5", "lint-staged": "^15.2.2", "ts-jest": "^29.0.5", "typescript": "^4.9.3" @@ -2178,6 +2179,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-chromedriver": { "version": "23.3.13", "resolved": "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-23.3.13.tgz", @@ -7821,6 +7834,12 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.2.tgz", "integrity": "sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw==" }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true + }, "electron-chromedriver": { "version": "23.3.13", "resolved": "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-23.3.13.tgz", diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 9ea42b05f4..3628d6e810 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -16,7 +16,8 @@ "test": "cross-env TEST_MODE=true jest --runInBand --detectOpenHandles --forceExit", "test:localBinary": "cross-env TEST_MODE=true IS_LOCAL=true jest --runInBand --detectOpenHandles --forceExit --verbose --", "test:prod": "jest --runInBand --detectOpenHandles --forceExit", - "test:watch": "jest --watchAll" + "test:watch": "jest --watchAll", + "linux:copy": "node scripts/copyAppImage.js" }, "devDependencies": { "@quiet/eslint-config": "^2.0.2-alpha.0", @@ -25,7 +26,8 @@ "babel-jest": "^29.3.1", "lint-staged": "^15.2.2", "ts-jest": "^29.0.5", - "typescript": "^4.9.3" + "typescript": "^4.9.3", + "dotenv": "16.4.5" }, "dependencies": { "@quiet/common": "^2.0.2-alpha.1", diff --git a/packages/e2e-tests/scripts/copyAppImage.js b/packages/e2e-tests/scripts/copyAppImage.js new file mode 100644 index 0000000000..8843b85535 --- /dev/null +++ b/packages/e2e-tests/scripts/copyAppImage.js @@ -0,0 +1,16 @@ +// Copy built AppImage to Quiet directory and set the version in .env file +const { execFileSync } = require('child_process') +const path = require('path') +const fs = require('fs') + +const desktop = path.join(__dirname, '..', '..', 'desktop') +const e2e = path.join(__dirname, '..') +const appVersion = JSON.parse(require('fs').readFileSync(path.join(desktop, 'package.json'), 'utf8')).version +const fileName = `Quiet-${appVersion}.AppImage` + +execFileSync('rm', ['-rf', path.join(desktop, 'dist', 'squashfs-root')]) + +console.log(`Copying file ${fileName} for e2e tests`) +execFileSync('cp', [path.join(desktop, 'dist', fileName), path.join(e2e, 'Quiet', fileName)]) + +fs.writeFileSync(path.join(e2e, '.env'), `FILE_NAME=${fileName}`) diff --git a/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts b/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts index 8f0c349de6..e238cbb81a 100644 --- a/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts +++ b/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts @@ -8,7 +8,7 @@ import { RegisterUsernameModal, Sidebar, } from '../selectors' -import { BACKWARD_COMPATIBILITY_BASE_VERSION, copyInstallerFile, downloadInstaller } from '../utils' +import { BACKWARD_COMPATIBILITY_BASE_VERSION, BuildSetup, copyInstallerFile, downloadInstaller } from '../utils' jest.setTimeout(1200000) describe('Backwards Compatibility', () => { @@ -26,7 +26,7 @@ describe('Backwards Compatibility', () => { const loopMessages = 'ąbc'.split('') const newChannelName = 'mid-night-club' - const isAlpha = process.env.FILE_NAME?.toString().includes('alpha') + const isAlpha = BuildSetup.getEnvFileName()?.toString().includes('alpha') beforeAll(async () => { // download the old version of the app diff --git a/packages/e2e-tests/src/utils.ts b/packages/e2e-tests/src/utils.ts index 7bf4a0ca6e..cb38d7a977 100644 --- a/packages/e2e-tests/src/utils.ts +++ b/packages/e2e-tests/src/utils.ts @@ -5,6 +5,7 @@ import getPort from 'get-port' import path from 'path' import fs from 'fs' import { DESKTOP_DATA_DIR, getAppDataPath } from '@quiet/common' +import { config } from 'dotenv' export const BACKWARD_COMPATIBILITY_BASE_VERSION = '2.0.1' // Pre-latest production version const appImagesPath = `${__dirname}/../Quiet` @@ -45,11 +46,17 @@ export class BuildSetup { this.debugPort = await getPort() } + static getEnvFileName() { + const { parsed, error } = config() + console.log('Dotenv config', { parsed, error }) + return process.env.FILE_NAME + } + private getBinaryLocation() { console.log('filename', this.fileName) switch (process.platform) { case 'linux': - return `${__dirname}/../Quiet/${this.fileName ? this.fileName : process.env.FILE_NAME}` + return `${__dirname}/../Quiet/${this.fileName ? this.fileName : BuildSetup.getEnvFileName()}` case 'win32': return `${process.env.LOCALAPPDATA}\\Programs\\@quietdesktop\\Quiet.exe` case 'darwin': @@ -71,7 +78,7 @@ export class BuildSetup { } public getVersionFromEnv() { - const envFileName = process.env.FILE_NAME + const envFileName = BuildSetup.getEnvFileName() if (!envFileName) { throw new Error('file name not specified') } diff --git a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx index 381d6831af..f992945a03 100644 --- a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx +++ b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx @@ -2,7 +2,7 @@ import React, { FC, useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { identity, communities } from '@quiet/state-manager' -import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationPair } from '@quiet/types' +import { InvitationData } from '@quiet/types' import { JoinCommunity } from '../../components/JoinCommunity/JoinCommunity.component' import { navigationActions } from '../../store/navigation/navigation.slice' import { ScreenNames } from '../../const/ScreenNames.enum' @@ -36,13 +36,7 @@ export const JoinCommunityScreen: FC = ({ route }) => const joinCommunityAction = useCallback( (data: InvitationData) => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: data.pairs, - psk: data.psk, - ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, - } - dispatch(communities.actions.createNetwork(payload)) + dispatch(communities.actions.joinNetwork(data)) dispatch( navigationActions.navigation({ screen: ScreenNames.UsernameRegistrationScreen, diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts index 80d718cd7e..a74e6f8eb2 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts @@ -8,16 +8,21 @@ import { initActions } from '../init.slice' import { navigationActions } from '../../navigation/navigation.slice' import { ScreenNames } from '../../../const/ScreenNames.enum' import { deepLinkSaga } from './deepLink.saga' -import { type Community, CommunityOwnership, ConnectionProcessInfo, type Identity, InvitationData } from '@quiet/types' -import { composeInvitationShareUrl, validInvitationCodeTestData, getValidInvitationUrlTestData } from '@quiet/common' +import { type Community, CommunityOwnership, type Identity, InvitationData, InvitationDataVersion } from '@quiet/types' +import { + composeInvitationShareUrl, + validInvitationCodeTestData, + getValidInvitationUrlTestData, + validInvitationDatav1, +} from '@quiet/common' describe('deepLinkSaga', () => { let store: Store - const { code, data } = getValidInvitationUrlTestData(validInvitationCodeTestData[0]) + const { code } = getValidInvitationUrlTestData(validInvitationDatav1[0]) const validCode = code() - const validData = data + const validData = validInvitationDatav1[0] const id = '00d045ab' @@ -59,11 +64,9 @@ describe('deepLinkSaga', () => { .withState(store.getState()) .put(initActions.resetDeepLink()) .put( - communities.actions.createNetwork({ - ownership: CommunityOwnership.User, - peers: validData.pairs, - psk: validData.psk, - ownerOrbitDbIdentity: validData.ownerOrbitDbIdentity, + communities.actions.joinNetwork({ + version: InvitationDataVersion.v1, + ...validData, }) ) .put( @@ -74,45 +77,6 @@ describe('deepLinkSaga', () => { .run() }) - // FIXME: Currently there's no way to actually check whether the redirection destionation is correct - test.skip('opens channel list screen if the same url has been used', async () => { - store.dispatch( - initActions.setWebsocketConnected({ - dataPort: 5001, - socketIOSecret: 'secret', - }) - ) - - store.dispatch(communities.actions.setInvitationCodes(validData.pairs)) - store.dispatch( - communities.actions.addNewCommunity({ - ...community, - name: 'rockets', - }) - ) - - store.dispatch( - // @ts-expect-error - identity.actions.addNewIdentity({ ..._identity, userCertificate: 'certificate' }) - ) - - store.dispatch(communities.actions.setCurrentCommunity(community.id)) - - const reducer = combineReducers(reducers) - await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) - .withReducer(reducer) - .withState(store.getState()) - .not.put( - communities.actions.createNetwork({ - ownership: CommunityOwnership.User, - peers: validData.pairs, - psk: validData.psk, - ownerOrbitDbIdentity: validData.ownerOrbitDbIdentity, - }) - ) - .run() - }) - test('displays error if user already belongs to a community', async () => { store.dispatch( initActions.setWebsocketConnected({ @@ -121,10 +85,6 @@ describe('deepLinkSaga', () => { }) ) - // Store other communitys' invitation data in redux - const invitationData = getValidInvitationUrlTestData(validInvitationCodeTestData[1]) - store.dispatch(communities.actions.setInvitationCodes(invitationData.data.pairs)) - store.dispatch( communities.actions.addNewCommunity({ ...community, @@ -165,13 +125,12 @@ describe('deepLinkSaga', () => { }) ) + community.psk = validData.psk + store.dispatch(communities.actions.addNewCommunity(community)) store.dispatch(communities.actions.setCurrentCommunity(community.id)) - const invitationCodes = getInvitationCodes(validCode) - store.dispatch(communities.actions.setInvitationCodes(invitationCodes.pairs)) - const reducer = combineReducers(reducers) await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) .withReducer(reducer) @@ -190,11 +149,10 @@ describe('deepLinkSaga', () => { }) .put.like({ action: { - type: communities.actions.createNetwork.type, + type: communities.actions.joinNetwork.type, payload: { - ownership: CommunityOwnership.User, - peers: validData.pairs, - psk: validData.psk, + version: InvitationDataVersion.v1, + ...validData, }, }, }) @@ -236,11 +194,9 @@ describe('deepLinkSaga', () => { }, }) .not.put( - communities.actions.createNetwork({ - ownership: CommunityOwnership.User, - peers: validData.pairs, - psk: validData.psk, - ownerOrbitDbIdentity: validData.ownerOrbitDbIdentity, + communities.actions.joinNetwork({ + version: InvitationDataVersion.v1, + ...validData, }) ) .run() diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts index 4c98593786..28d7a03fc8 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts @@ -7,8 +7,7 @@ import { initSelectors } from '../init.selectors' import { initActions } from '../init.slice' import { appImages } from '../../../assets' import { replaceScreen } from '../../../RootNavigation' -import { CommunityOwnership, CreateNetworkPayload, InvitationData } from '@quiet/types' -import { areObjectsEqual } from '../../../utils/functions/areObjectsEqual/areObjectsEqual' +import { InvitationData, InvitationDataVersion } from '@quiet/types' export function* deepLinkSaga(action: PayloadAction['payload']>): Generator { const code = action.payload @@ -49,49 +48,25 @@ export function* deepLinkSaga(action: PayloadAction - currentInvitationCodes.some(currentCode => areObjectsEqual(storedCode, currentCode)) - ) + isJoiningAnotherCommunity = Boolean(storedPsk && storedPsk !== currentPsk) + break } - console.log('Is invitation data valid', isInvitationDataValid) - const isAlreadyConnected = Boolean(community?.name) - - const alreadyBelongsWithAnotherCommunity = !isInvitationDataValid && isAlreadyConnected - const connectingWithAnotherCommunity = !isInvitationDataValid && !isAlreadyConnected - const alreadyBelongsWithCurrentCommunity = isInvitationDataValid && isAlreadyConnected - const connectingWithCurrentCommunity = isInvitationDataValid && !isAlreadyConnected - - if (alreadyBelongsWithAnotherCommunity) { - console.log('INIT_NAVIGATION: ABORTING: Already belongs with another community.') - } - - if (connectingWithAnotherCommunity) { - console.log('INIT_NAVIGATION: ABORTING: Proceeding with connection to another community.') - } - - if (alreadyBelongsWithCurrentCommunity) { - console.log('INIT_NAVIGATION: ABORTING: Already connected with the current community.') - } - - if (connectingWithCurrentCommunity) { - console.log('INIT_NAVIGATION: Proceeding with connection to the community.') - } + const connectingWithAnotherCommunity = isJoiningAnotherCommunity && !isAlreadyConnected // User already belongs to a community - if (alreadyBelongsWithAnotherCommunity || alreadyBelongsWithCurrentCommunity) { + if (isAlreadyConnected) { console.log('INIT_NAVIGATION: Displaying error (user already belongs to a community).') yield* put( @@ -127,16 +102,7 @@ export function* deepLinkSaga(action: PayloadAction]) => { + const action = input[0] + if (action === SocketActionTypes.CREATE_NETWORK) { + const data: NetworkInfo = { + hiddenService: { + onionAddress: 'onionAddress', + privateKey: 'privateKey', + }, + peerId: createPeerIdTestHelper(), + } + return data + } +} describe('Deep linking', () => { let socket: MockedSocket beforeEach(async () => { socket = new MockedSocket() - // @ts-ignore - socket.emitWithAck = jest.fn() ioMock.mockImplementation(() => socket) + jest.spyOn(socket, 'emit').mockImplementation(mockEmitImpl) + // @ts-ignore + socket.emitWithAck = mockEmitImpl }) test('does not override network data if triggered twice', async () => { @@ -52,12 +68,16 @@ describe('Deep linking', () => { [ "Init/deepLink", "Init/resetDeepLink", + "Communities/joinNetwork", "Communities/createNetwork", - "Communities/addNewCommunity", "Navigation/replaceScreen", + "Communities/addNewCommunity", "Communities/setCurrentCommunity", "Communities/setInvitationCodes", + "Identity/addNewIdentity", "Init/deepLink", + "Init/resetDeepLink", + "Navigation/replaceScreen", ] `) diff --git a/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts b/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts index f646ea2fd8..184546cc84 100644 --- a/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts +++ b/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts @@ -7,7 +7,7 @@ import { communitiesActions } from '../communities/communities.slice' import { connectionActions } from './connection.slice' import { type FactoryGirl } from 'factory-girl' import { type Community } from '@quiet/types' -import { createLibp2pAddress, invitationShareUrl } from '@quiet/common' +import { composeInvitationShareUrl, createLibp2pAddress, p2pAddressesToPairs } from '@quiet/common' describe('communitiesSelectors', () => { setupCrypto() @@ -116,7 +116,8 @@ describe('communitiesSelectors', () => { ownerOrbitDbIdentity, }) const selectorInvitationUrl = connectionSelectors.invitationUrl(store.getState()) - const expectedUrl = invitationShareUrl(peerList, psk, ownerOrbitDbIdentity) + const pairs = p2pAddressesToPairs(peerList) + const expectedUrl = composeInvitationShareUrl({ pairs, psk, ownerOrbitDbIdentity }) expect(expectedUrl).not.toEqual('') expect(selectorInvitationUrl).toEqual(expectedUrl) }) diff --git a/packages/state-manager/src/sagas/appConnection/connection.selectors.ts b/packages/state-manager/src/sagas/appConnection/connection.selectors.ts index 1bce105c4c..72f8cfd9f1 100644 --- a/packages/state-manager/src/sagas/appConnection/connection.selectors.ts +++ b/packages/state-manager/src/sagas/appConnection/connection.selectors.ts @@ -6,7 +6,7 @@ import { peersStatsAdapter } from './connection.adapter' import { connectedPeers, isCurrentCommunityInitialized } from '../network/network.selectors' import { type NetworkStats } from './connection.types' import { type User } from '../users/users.types' -import { filterAndSortPeers, invitationShareUrl } from '@quiet/common' +import { composeInvitationShareUrl, filterAndSortPeers, p2pAddressesToPairs } from '@quiet/common' import { areMessagesLoaded, areChannelsLoaded } from '../publicChannels/publicChannels.selectors' import { identitySelectors } from '../identity/identity.selectors' import { communitiesSelectors } from '../communities/communities.selectors' @@ -54,7 +54,8 @@ export const invitationUrl = createSelector( if (!communityPsk) return '' if (!ownerOrbitDbIdentity) return '' const initialPeers = sortedPeerList.slice(0, 3) - return invitationShareUrl(initialPeers, communityPsk, ownerOrbitDbIdentity) + const pairs = p2pAddressesToPairs(initialPeers) + return composeInvitationShareUrl({ pairs, psk: communityPsk, ownerOrbitDbIdentity }) } ) diff --git a/packages/state-manager/src/sagas/communities/communities.master.saga.ts b/packages/state-manager/src/sagas/communities/communities.master.saga.ts index c5e1e1a275..e904dd16ad 100644 --- a/packages/state-manager/src/sagas/communities/communities.master.saga.ts +++ b/packages/state-manager/src/sagas/communities/communities.master.saga.ts @@ -5,10 +5,12 @@ import { connectionActions } from '../appConnection/connection.slice' import { createCommunitySaga } from './createCommunity/createCommunity.saga' import { initCommunities, launchCommunitySaga } from './launchCommunity/launchCommunity.saga' import { createNetworkSaga } from './createNetwork/createNetwork.saga' +import { joinNetworkSaga } from './joinNetwork/joinNetwork.saga' export function* communitiesMasterSaga(socket: Socket): Generator { yield all([ takeEvery(communitiesActions.createNetwork.type, createNetworkSaga, socket), + takeEvery(communitiesActions.joinNetwork.type, joinNetworkSaga, socket), takeEvery(connectionActions.torBootstrapped.type, initCommunities), takeEvery(communitiesActions.createCommunity.type, createCommunitySaga, socket), takeEvery(communitiesActions.launchCommunity.type, launchCommunitySaga, socket), diff --git a/packages/state-manager/src/sagas/communities/communities.selectors.test.ts b/packages/state-manager/src/sagas/communities/communities.selectors.test.ts index 16ee02a6cb..f545a49fe3 100644 --- a/packages/state-manager/src/sagas/communities/communities.selectors.test.ts +++ b/packages/state-manager/src/sagas/communities/communities.selectors.test.ts @@ -1,10 +1,8 @@ -import { createLibp2pAddress, invitationShareUrl } from '@quiet/common' import { setupCrypto } from '@quiet/identity' import { type Store } from '@reduxjs/toolkit' import { getFactory } from '../../utils/tests/factories' import { prepareStore } from '../../utils/tests/prepareStore' import { type identityActions } from '../identity/identity.slice' -import { usersActions } from '../users/users.slice' import { communitiesSelectors } from './communities.selectors' import { communitiesActions } from './communities.slice' import { type Community, type Identity } from '@quiet/types' diff --git a/packages/state-manager/src/sagas/communities/communities.slice.ts b/packages/state-manager/src/sagas/communities/communities.slice.ts index dae53ca209..228ef912fb 100644 --- a/packages/state-manager/src/sagas/communities/communities.slice.ts +++ b/packages/state-manager/src/sagas/communities/communities.slice.ts @@ -36,10 +36,11 @@ export const communitiesSlice = createSlice({ }) }, createNetwork: (state, _action: PayloadAction) => state, + joinNetwork: (state, _action: PayloadAction) => state, resetApp: (state, _action) => state, createCommunity: (state, _action: PayloadAction) => state, launchCommunity: (state, _action: PayloadAction) => state, - customProtocol: (state, _action: PayloadAction) => state, + customProtocol: (state, _action: PayloadAction) => state, setInvitationCodes: (state, action: PayloadAction) => { state.invitationCodes = action.payload }, diff --git a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts index 33003d6df5..719d9c2354 100644 --- a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts +++ b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts @@ -5,7 +5,7 @@ import { generateId } from '../../../utils/cryptography/cryptography' import { communitiesActions } from '../communities.slice' import { identityActions } from '../../identity/identity.slice' import { createRootCA } from '@quiet/identity' -import { type Community, CommunityOwnership, type Identity, SocketActionTypes } from '@quiet/types' +import { type Community, CommunityOwnership, type Identity, SocketActionTypes, NetworkInfo } from '@quiet/types' import { Socket, applyEmitParams } from '../../../types' export function* createNetworkSaga( @@ -17,7 +17,11 @@ export function* createNetworkSaga( // Community IDs are only local identifiers const id = yield* call(generateId) - const network = yield* apply(socket, socket.emitWithAck, applyEmitParams(SocketActionTypes.CREATE_NETWORK, id)) + const network: NetworkInfo = yield* apply( + socket, + socket.emitWithAck, + applyEmitParams(SocketActionTypes.CREATE_NETWORK, id) + ) // TODO: Move CA generation to backend when creating Community let CA: null | { diff --git a/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.test.ts b/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.test.ts new file mode 100644 index 0000000000..151d16cf0b --- /dev/null +++ b/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.test.ts @@ -0,0 +1,45 @@ +import { getValidInvitationUrlTestData, validInvitationDatav1 } from '@quiet/common' +import { CommunityOwnership, CreateNetworkPayload, InvitationDataV1 } from '@quiet/types' +import { FactoryGirl } from 'factory-girl' +import { expectSaga } from 'redux-saga-test-plan' +import { Socket } from '../../../types' +import { getFactory } from '../../../utils/tests/factories' +import { prepareStore } from '../../../utils/tests/prepareStore' +import { Store } from '../../store.types' +import { communitiesActions } from '../communities.slice' +import { joinNetworkSaga } from './joinNetwork.saga' + +describe('Join network saga', () => { + let store: Store + let factory: FactoryGirl + let validInvitationData: InvitationDataV1 + let validInvitationDeepUrl: string + const socket = { + emit: jest.fn(), + emitWithAck: jest.fn(() => { + return {} + }), + on: jest.fn(), + } as unknown as Socket + + beforeEach(async () => { + store = prepareStore().store + factory = await getFactory(store) + + validInvitationData = getValidInvitationUrlTestData(validInvitationDatav1[0]).data + validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationDatav1[0]).deepUrl() + }) + + it('creates network for v1 invitation data', async () => { + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: validInvitationData.pairs, + psk: validInvitationData.psk, + ownerOrbitDbIdentity: validInvitationData.ownerOrbitDbIdentity, + } + await expectSaga(joinNetworkSaga, socket, communitiesActions.joinNetwork(validInvitationData)) + .withState(store.getState()) + .put(communitiesActions.createNetwork(payload)) + .run() + }) +}) diff --git a/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.ts b/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.ts new file mode 100644 index 0000000000..53cb44b3b8 --- /dev/null +++ b/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.ts @@ -0,0 +1,47 @@ +import { CommunityOwnership, CreateNetworkPayload, InvitationDataVersion, SocketActionTypes } from '@quiet/types' +import { PayloadAction } from '@reduxjs/toolkit' +import { apply, put } from 'typed-redux-saga' +import { Socket, applyEmitParams } from '../../../types' +import { communitiesActions } from '../communities.slice' + +export function* joinNetworkSaga( + socket: Socket, + action: PayloadAction['payload']> +) { + console.log('join network saga', action.payload) + const data = action.payload + let payload: CreateNetworkPayload + + data.version = data.version || InvitationDataVersion.v1 + switch (data.version) { + case InvitationDataVersion.v1: + console.log('join network saga invitation data v1') + payload = { + ownership: CommunityOwnership.User, + peers: data.pairs, + psk: data.psk, + ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + } + break + case InvitationDataVersion.v2: + console.log('join network saga invitation data v2') + const response: CreateNetworkPayload = yield* apply( + socket, + socket.emitWithAck, + applyEmitParams(SocketActionTypes.DOWNLOAD_INVITE_DATA, { + serverAddress: data.serverAddress, + cid: data.cid, + }) + ) + payload = { + ownership: CommunityOwnership.User, + peers: response.peers, + psk: response.psk, + ownerOrbitDbIdentity: response.ownerOrbitDbIdentity, + } + break + } + + console.log('join network saga payload', payload) + yield* put(communitiesActions.createNetwork(payload)) +} diff --git a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts index 278a7d22e2..c7f620567d 100644 --- a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts +++ b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts @@ -29,6 +29,7 @@ export function* launchCommunitySaga( socket: Socket, action: PayloadAction['payload']> ): Generator { + console.log('LAUNCH COMMUNITY SAGA') const communityId = action.payload if (!communityId) { diff --git a/packages/state-manager/src/types.ts b/packages/state-manager/src/types.ts index 44260530d5..c6b0ab6543 100644 --- a/packages/state-manager/src/types.ts +++ b/packages/state-manager/src/types.ts @@ -51,6 +51,7 @@ export interface EmitEvents { [SocketActionTypes.CREATE_NETWORK]: EmitEvent void> [SocketActionTypes.ADD_CSR]: EmitEvent [SocketActionTypes.SET_USER_PROFILE]: EmitEvent + [SocketActionTypes.DOWNLOAD_INVITE_DATA]: EmitEvent<{ serverAddress: string; cid: string }> [SocketActionTypes.LOAD_MIGRATION_DATA]: EmitEvent> } diff --git a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts index 7d5ac34e7e..c5ba56193d 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts @@ -1,27 +1,48 @@ +import { InvitationDataV1, InvitationDataVersion } from '@quiet/types' import { getInvitationCodes } from './invitationCode' -import { OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, PSK_PARAM_KEY, QUIET_JOIN_PAGE } from '@quiet/common' +import { + CID_PARAM_KEY, + INVITER_ADDRESS_PARAM_KEY, + OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, + PSK_PARAM_KEY, + QUIET_JOIN_PAGE, + SERVER_ADDRESS_PARAM_KEY, + TOKEN_PARAM_KEY, + validInvitationDatav1, + validInvitationDatav2, +} from '@quiet/common' + +const getUrlParamsPart = (url: string) => url.split(QUIET_JOIN_PAGE + '?')[1] describe('Invitation code helper', () => { - const peerId1 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA' - const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' - const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' - const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const psk = 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw=' - const ownerOrbitDbIdentity = 'testOwnerOrbitDbIdentity' - const encodedPsk = encodeURIComponent(psk) - const encodedOwnerOrbitDbIdentity = encodeURIComponent(ownerOrbitDbIdentity) + const address = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' + const peerId = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' + const data: InvitationDataV1 = { + ...validInvitationDatav1[0], + pairs: [...validInvitationDatav1[0].pairs, { peerId: peerId, onionAddress: address }], + } + const urlParams = [ + [data.pairs[0].peerId, data.pairs[0].onionAddress], + [data.pairs[1].peerId, data.pairs[1].onionAddress], + [PSK_PARAM_KEY, data.psk], + [OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity], + ] + + const datav2 = validInvitationDatav2[0] + const urlParamsv2 = [ + [CID_PARAM_KEY, datav2.cid], + [TOKEN_PARAM_KEY, datav2.token], + [SERVER_ADDRESS_PARAM_KEY, datav2.serverAddress], + [INVITER_ADDRESS_PARAM_KEY, datav2.inviterAddress], + ] it('retrieves invitation code if url is a proper share url', () => { - const result = getInvitationCodes( - `${QUIET_JOIN_PAGE}#${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${encodedPsk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${encodedOwnerOrbitDbIdentity}` - ) + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + const result = getInvitationCodes(url.href.replace('?', '#')) expect(result).toEqual({ - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk, - ownerOrbitDbIdentity, + version: InvitationDataVersion.v1, + ...data, }) }) @@ -30,38 +51,33 @@ describe('Invitation code helper', () => { }) it('throws error if code does not contain psk', () => { - expect(() => getInvitationCodes(`${peerId1}=${address1}&${peerId2}=${address2}`)).toThrow() + const url = new URL(QUIET_JOIN_PAGE) + url.searchParams.append(urlParams[0][0], urlParams[0][1]) + url.searchParams.append(urlParams[1][0], urlParams[1][1]) + expect(() => getInvitationCodes(getUrlParamsPart(url.href))).toThrow() }) it('throws error if psk has invalid format', () => { - expect(() => getInvitationCodes(`${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=12345`)).toThrow() + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + url.searchParams.set(PSK_PARAM_KEY, '12345') + expect(() => getInvitationCodes(getUrlParamsPart(url.href))).toThrow() }) it('retrieves invitation code if url is a proper code', () => { - const result = getInvitationCodes( - `${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${encodedPsk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${encodedOwnerOrbitDbIdentity}` - ) + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + const result = getInvitationCodes(getUrlParamsPart(url.href)) expect(result).toEqual({ - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk, - ownerOrbitDbIdentity, + version: InvitationDataVersion.v1, + ...data, }) }) - it('retrieves invitation code if url is a proper code', () => { - const result = getInvitationCodes( - `${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${encodedPsk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${encodedOwnerOrbitDbIdentity}` - ) - expect(result).toEqual({ - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk, - ownerOrbitDbIdentity, - }) + it('retrieves invitation code if url is a proper v2 code', () => { + const url = new URL(QUIET_JOIN_PAGE) + urlParamsv2.forEach(([key, value]) => url.searchParams.append(key, value)) + const result = getInvitationCodes(getUrlParamsPart(url.href)) + expect(result).toEqual(datav2) }) }) diff --git a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts index 1fe956f338..e147a2fea8 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts @@ -6,7 +6,6 @@ export const getInvitationCodes = (codeOrUrl: string): InvitationData => { * Extract codes from invitation share url or return passed value for further error handling * @param codeOrUrl: full invitation link or just the code part of the link */ - let data: InvitationData | null = null let potentialCode let validUrl: URL | null = null @@ -30,11 +29,5 @@ export const getInvitationCodes = (codeOrUrl: string): InvitationData => { code = potentialCode } - data = parseInvitationCode(code) - - if (!data || data?.pairs.length === 0) { - throw new Error(`No invitation codes. Code/url passed: ${codeOrUrl}`) - } - - return data + return parseInvitationCode(code) } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3d16fdbe16..0a7902637e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -9,3 +9,4 @@ export * from './message' export * from './files' export * from './channel' export * from './network' +export * from './test' diff --git a/packages/types/src/network.ts b/packages/types/src/network.ts index 894ca4f427..977bb063c0 100644 --- a/packages/types/src/network.ts +++ b/packages/types/src/network.ts @@ -8,8 +8,24 @@ export type InvitationPair = { onionAddress: string } -export type InvitationData = { +export enum InvitationDataVersion { + v1 = 'v1', + v2 = 'v2', +} + +export type InvitationDataV1 = { + version?: InvitationDataVersion.v1 pairs: InvitationPair[] psk: string ownerOrbitDbIdentity: string } + +export type InvitationDataV2 = { + version?: InvitationDataVersion.v2 + cid: string + token: string + serverAddress: string + inviterAddress: string +} + +export type InvitationData = InvitationDataV1 | InvitationDataV2 diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index 573fb1a313..e9edb6c5b1 100644 --- a/packages/types/src/socket.ts +++ b/packages/types/src/socket.ts @@ -19,6 +19,7 @@ export enum SocketActionTypes { COMMUNITY_LAUNCHED = 'communityLaunched', COMMUNITY_UPDATED = 'communityUpdated', CREATE_COMMUNITY = 'createCommunity', + DOWNLOAD_INVITE_DATA = 'downloadInviteData', LAUNCH_COMMUNITY = 'launchCommunity', LEAVE_COMMUNITY = 'leaveCommunity', diff --git a/packages/desktop/src/renderer/testUtils/socket.ts b/packages/types/src/test.ts similarity index 100% rename from packages/desktop/src/renderer/testUtils/socket.ts rename to packages/types/src/test.ts