diff --git a/packages/backend/src/nest/storage/storage.service.ts b/packages/backend/src/nest/storage/storage.service.ts index d740373e64..7524b13bd3 100644 --- a/packages/backend/src/nest/storage/storage.service.ts +++ b/packages/backend/src/nest/storage/storage.service.ts @@ -171,7 +171,7 @@ export class StorageService extends EventEmitter { return } for (const a of addr) { - this.logger(`Pubsub - subscribe to ${addr}`) + this.logger(`Pubsub - subscribe to ${a}`) // @ts-ignore await this.orbitDb._pubsub.subscribe( a, diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index 6d17e38251..b43cb2053f 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -1,3 +1,4 @@ +import { InvitationPair } from '@quiet/types' import { InvitationParams, Site } from './static' export const retrieveInvitationCode = (url: string): string => { @@ -20,20 +21,78 @@ export const retrieveInvitationCode = (url: string): string => { return '' } -export const argvInvitationCode = (argv: string[]): string => { +export const retrieveInvitationCodePairs = (url: string): InvitationPair[] => { + /** + * Extract invitation code from deep url. + * Valid format: quiet://?=&= + */ + let data: URL + try { + data = new URL(url) + } catch (e) { + return [] + } + if (!data || data.protocol !== 'quiet:') return [] + const params = data.searchParams + const codes: InvitationPair[] = [] + for (const [peerId, address] of params) { + // TODO: basic check if peerid and address have proper format? + if (peerId.length !== 46 || address.length !== 56) { + console.log(`peerId '${peerId}' or address ${address} is not valid`) + continue + } + codes.push({ + peerId, + address, + }) + } + console.log('Retrieved codes:', codes) + return codes +} + +export const invitationShareUrlMultipleAddresses = (pairs: InvitationPair[] = []): string => { + // Valid format: https://tryquiet.org/join/#=&= + const code = pairs.map(pair => `${pair.peerId}=${pair.address}`).join('&') + const url = new URL(`https://${Site.DOMAIN}/${Site.JOIN_PAGE}#${code}`) + return url.href +} + +export const invitationDeepUrlMultipleAddresses = (pairs: InvitationPair[] = []): string => { + const url = new URL('quiet://') + for (const pair of pairs) { + url.searchParams.append(pair.peerId, pair.address) + } + return url.href +} + +export const argvInvitationCode = (argv: string[]): InvitationPair[] => { /** * Extract invitation code from deep url if url is present in argv */ - let invitationCode = '' + let invitationCodes = [] for (const arg of argv) { - invitationCode = retrieveInvitationCode(arg) - if (invitationCode) { + invitationCodes = retrieveInvitationCodePairs(arg) + if (invitationCodes.length > 0) { break } } - return invitationCode + return invitationCodes } +// export const argvInvitationCode = (argv: string[]): string => { +// /** +// * Extract invitation code from deep url if url is present in argv +// */ +// let invitationCode = '' +// for (const arg of argv) { +// invitationCode = retrieveInvitationCode(arg) +// if (invitationCode) { +// break +// } +// } +// return invitationCode +// } + export const invitationDeepUrl = (code = ''): string => { const url = new URL('quiet://') url.searchParams.append(InvitationParams.CODE, code) diff --git a/packages/desktop/src/main/invitation.ts b/packages/desktop/src/main/invitation.ts index 23f61c847e..59ac09262c 100644 --- a/packages/desktop/src/main/invitation.ts +++ b/packages/desktop/src/main/invitation.ts @@ -3,11 +3,12 @@ import path from 'path' import os from 'os' import { execSync } from 'child_process' import { BrowserWindow } from 'electron' +import { InvitationPair } from '@quiet/types' -export const processInvitationCode = (mainWindow: BrowserWindow, code: string) => { - if (!code) return +export const processInvitationCode = (mainWindow: BrowserWindow, codes: InvitationPair[]) => { + if (codes.length === 0) return mainWindow.webContents.send('invitation', { - code, + codes, }) } diff --git a/packages/desktop/src/main/main.ts b/packages/desktop/src/main/main.ts index 932780c8e0..bbb35bf6ac 100644 --- a/packages/desktop/src/main/main.ts +++ b/packages/desktop/src/main/main.ts @@ -11,9 +11,8 @@ import { Crypto } from '@peculiar/webcrypto' import logger from './logger' import { DATA_DIR, DEV_DATA_DIR } from '../shared/static' import { fork, ChildProcess } from 'child_process' -import { getFilesData } from '@quiet/common' +import { argvInvitationCode, getFilesData, retrieveInvitationCodePairs } from '@quiet/common' import { updateDesktopFile, processInvitationCode } from './invitation' -import { argvInvitationCode, retrieveInvitationCode } from '@quiet/common' const ElectronStore = require('electron-store') ElectronStore.initRenderer() @@ -148,7 +147,7 @@ app.on('open-url', (event, url) => { event.preventDefault() if (mainWindow) { invitationUrl = null - const invitationCode = retrieveInvitationCode(url) + const invitationCode = retrieveInvitationCodePairs(url) processInvitationCode(mainWindow, invitationCode) } }) @@ -475,12 +474,12 @@ app.on('ready', async () => { throw new Error(`mainWindow is on unexpected type ${mainWindow}`) } if (process.platform === 'darwin' && invitationUrl) { - const invitationCode = retrieveInvitationCode(invitationUrl) + const invitationCode = retrieveInvitationCodePairs(invitationUrl) processInvitationCode(mainWindow, invitationCode) invitationUrl = null } if (process.platform !== 'darwin' && process.argv) { - const invitationCode = argvInvitationCode(process.argv) + const invitationCode = argvInvitationCodeMultipleAddresses(process.argv) processInvitationCode(mainWindow, invitationCode) } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx index 38df07e800..9ed4991099 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { socketSelectors } from '../../../sagas/socket/socket.selectors' -import { CommunityOwnership, CreateNetworkPayload, TOR_BOOTSTRAP_COMPLETE } from '@quiet/types' +import { CommunityOwnership, CreateNetworkPayload, InvitationPair, TOR_BOOTSTRAP_COMPLETE } from '@quiet/types' import { communities, identity, connection } from '@quiet/state-manager' import PerformCommunityActionComponent from '../../../components/CreateJoinCommunity/PerformCommunityActionComponent' import { ModalName } from '../../../sagas/modals/modals.types' @@ -41,10 +41,10 @@ const JoinCommunity = () => { } }, [currentCommunity]) - const handleCommunityAction = (address: string) => { + const handleCommunityAction = (address: InvitationPair[]) => { const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, - registrar: address, + peers: address, } dispatch(communities.actions.createNetwork(payload)) } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx index e0442790ae..1b7f20dc23 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx @@ -21,7 +21,7 @@ import { IconButton, InputAdornment } from '@mui/material' import VisibilityOff from '@mui/icons-material/VisibilityOff' import Visibility from '@mui/icons-material/Visibility' import { ONION_ADDRESS_REGEX, parseName } from '@quiet/common' -import { getInvitationCode } from '@quiet/state-manager' +import { getInvitationCodes } from '@quiet/state-manager' const PREFIX = 'PerformCommunityActionComponent' @@ -129,7 +129,7 @@ interface PerformCommunityActionFormValues { export interface PerformCommunityActionProps { open: boolean communityOwnership: CommunityOwnership - handleCommunityAction: (value: string) => void + handleCommunityAction: (value: any) => void handleRedirection: () => void handleClose: () => void isConnectionReady?: boolean @@ -178,22 +178,30 @@ export const PerformCommunityActionComponent: React.FC submitForm(handleCommunityAction, values, setFormSent) const submitForm = ( - handleSubmit: (value: string) => void, + handleSubmit: (value: any) => void, values: PerformCommunityActionFormValues, setFormSent: (value: boolean) => void ) => { - let submitValue = communityOwnership === CommunityOwnership.Owner ? parseName(values.name) : values.name.trim() + if (communityOwnership === CommunityOwnership.Owner) { + setFormSent(true) + handleSubmit(parseName(values.name)) + return + } + + // let submitValue = communityOwnership === CommunityOwnership.Owner ? parseName(values.name) : values.name.trim() if (communityOwnership === CommunityOwnership.User) { - submitValue = getInvitationCode(submitValue) - if (!submitValue || !submitValue.match(ONION_ADDRESS_REGEX)) { + const codes = getInvitationCodes(values.name.trim()) + if (!codes.length) { + // if (!submitValue || !submitValue.match(ONION_ADDRESS_REGEX)) { // TODO: add basic validation setError('name', { message: InviteLinkErrors.InvalidCode }) return + // } } - } - setFormSent(true) - handleSubmit(submitValue) + setFormSent(true) + handleSubmit(codes) + } } const onChange = (name: string) => { diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index a56c5f0931..bf35aea3bc 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -25,6 +25,11 @@ ipcRenderer.on('invitation', (_event, invitation) => { store.dispatch(communities.actions.handleInvitationCode(invitation.code)) }) +ipcRenderer.on('invitationMA', (_event, invitation) => { + console.log('invitation', invitation, 'dispatching action') + store.dispatch(communities.actions.handleInvitationCodes(invitation.codes)) +}) + const container = document.getElementById('root') if (!container) throw new Error('No root html element!') let root = createRoot(container) diff --git a/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.test.ts b/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.test.ts index 470a0ae044..82cd8afd50 100644 --- a/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.test.ts +++ b/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.test.ts @@ -1,87 +1,87 @@ -import { communities, getFactory, Store } from '@quiet/state-manager' -import { Community, CommunityOwnership, CreateNetworkPayload } from '@quiet/types' -import { FactoryGirl } from 'factory-girl' -import { expectSaga } from 'redux-saga-test-plan' -import { handleInvitationCodeSaga } from './handleInvitationCode.saga' -import { SocketState } from '../socket/socket.slice' -import { prepareStore } from '../../testUtils/prepareStore' -import { StoreKeys } from '../../store/store.keys' -import { modalsActions } from '../modals/modals.slice' -import { ModalName } from '../modals/modals.types' +// import { communities, getFactory, Store } from '@quiet/state-manager' +// import { Community, CommunityOwnership, CreateNetworkPayload } from '@quiet/types' +// import { FactoryGirl } from 'factory-girl' +// import { expectSaga } from 'redux-saga-test-plan' +// import { handleInvitationCodeSaga } from './handleInvitationCode.saga' +// import { SocketState } from '../socket/socket.slice' +// import { prepareStore } from '../../testUtils/prepareStore' +// import { StoreKeys } from '../../store/store.keys' +// import { modalsActions } from '../modals/modals.slice' +// import { ModalName } from '../modals/modals.types' -describe('Handle invitation code', () => { - let store: Store - let factory: FactoryGirl - let community: Community - let validInvitationCode: string +// describe('Handle invitation code', () => { +// let store: Store +// let factory: FactoryGirl +// let community: Community +// let validInvitationCode: string - beforeEach(async () => { - store = ( - await prepareStore({ - [StoreKeys.Socket]: { - ...new SocketState(), - isConnected: true, - }, - }) - ).store +// beforeEach(async () => { +// store = ( +// await prepareStore({ +// [StoreKeys.Socket]: { +// ...new SocketState(), +// isConnected: true, +// }, +// }) +// ).store - factory = await getFactory(store) - validInvitationCode = 'bb5wacaftixjl3yhq2cp3ls2ife2e5wlwct3hjlb4lyk4iniypmgozyd' - }) +// factory = await getFactory(store) +// validInvitationCode = 'bb5wacaftixjl3yhq2cp3ls2ife2e5wlwct3hjlb4lyk4iniypmgozyd' +// }) - it('creates network if code is valid', async () => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - registrar: validInvitationCode, - } - await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(validInvitationCode)) - .withState(store.getState()) - .put(communities.actions.createNetwork(payload)) - .run() - }) +// it('creates network if code is valid', async () => { +// const payload: CreateNetworkPayload = { +// ownership: CommunityOwnership.User, +// registrar: validInvitationCode, +// } +// await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(validInvitationCode)) +// .withState(store.getState()) +// .put(communities.actions.createNetwork(payload)) +// .run() +// }) - it('does not try to create network if user is already in community', async () => { - community = await factory.create['payload']>('Community') - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - registrar: validInvitationCode, - } +// it('does not try to create network if user is already in community', async () => { +// community = await factory.create['payload']>('Community') +// const payload: CreateNetworkPayload = { +// ownership: CommunityOwnership.User, +// registrar: validInvitationCode, +// } - await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(validInvitationCode)) - .withState(store.getState()) - .put( - modalsActions.openModal({ - name: ModalName.warningModal, - args: { - title: 'You already belong to a community', - subtitle: "We're sorry but for now you can only be a member of a single community at a time.", - }, - }) - ) - .not.put(communities.actions.createNetwork(payload)) - .run() - }) +// await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(validInvitationCode)) +// .withState(store.getState()) +// .put( +// modalsActions.openModal({ +// name: ModalName.warningModal, +// args: { +// title: 'You already belong to a community', +// subtitle: "We're sorry but for now you can only be a member of a single community at a time.", +// }, +// }) +// ) +// .not.put(communities.actions.createNetwork(payload)) +// .run() +// }) - it('does not try to create network if code is invalid', async () => { - const code = 'invalid' - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - registrar: code, - } +// it('does not try to create network if code is invalid', async () => { +// const code = 'invalid' +// const payload: CreateNetworkPayload = { +// ownership: CommunityOwnership.User, +// peers: code, +// } - await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(code)) - .withState(store.getState()) - .put(communities.actions.clearInvitationCode()) - .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.', - }, - }) - ) - .not.put(communities.actions.createNetwork(payload)) - .run() - }) -}) +// await expectSaga(handleInvitationCodeSaga, communities.actions.handleInvitationCode(code)) +// .withState(store.getState()) +// .put(communities.actions.clearInvitationCode()) +// .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.', +// }, +// }) +// ) +// .not.put(communities.actions.createNetwork(payload)) +// .run() +// }) +// }) diff --git a/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.ts b/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.ts index 5fab99fb8d..1a88cce5ca 100644 --- a/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.ts +++ b/packages/desktop/src/renderer/sagas/invitation/handleInvitationCode.saga.ts @@ -8,7 +8,7 @@ import { ModalName } from '../modals/modals.types' import { modalsActions } from '../modals/modals.slice' export function* handleInvitationCodeSaga( - action: PayloadAction['payload']> + action: PayloadAction['payload']> ): Generator { while (true) { const connected = yield* select(socketSelectors.isConnected) @@ -32,26 +32,27 @@ export function* handleInvitationCodeSaga( return } - const code = action.payload.trim() + // const code = action.payload.trim() - if (code.match(ONION_ADDRESS_REGEX)) { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - registrar: code, - } - yield* put(communities.actions.createNetwork(payload)) - return + // if (code.match(ONION_ADDRESS_REGEX)) { + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: action.payload, } + yield* put(communities.actions.createNetwork(payload)) + return + // } - yield* put(communities.actions.clearInvitationCode()) + // TODO: handle invalid code + // yield* put(communities.actions.clearInvitationCode()) - 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.', - }, - }) - ) + // 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.', + // }, + // }) + // ) } diff --git a/packages/mobile/src/components/JoinCommunity/JoinCommunity.component.tsx b/packages/mobile/src/components/JoinCommunity/JoinCommunity.component.tsx index 1f4b90c4aa..96dbf5f5fe 100644 --- a/packages/mobile/src/components/JoinCommunity/JoinCommunity.component.tsx +++ b/packages/mobile/src/components/JoinCommunity/JoinCommunity.component.tsx @@ -7,7 +7,7 @@ import { Typography } from '../Typography/Typography.component' import { TextWithLink } from '../TextWithLink/TextWithLink.component' import { JoinCommunityProps } from './JoinCommunity.types' -import { getInvitationCode } from '@quiet/state-manager' +import { getInvitationCodes } from '@quiet/state-manager' import { ONION_ADDRESS_REGEX } from '@quiet/common' export const JoinCommunity: FC = ({ @@ -32,16 +32,14 @@ export const JoinCommunity: FC = ({ Keyboard.dismiss() setLoading(true) - let submitValue: string | undefined = joinCommunityInput - - if (submitValue === undefined || submitValue?.length === 0) { + if (joinCommunityInput === undefined || joinCommunityInput?.length === 0) { setLoading(false) setInputError('Community address can not be empty') return } - submitValue = getInvitationCode(submitValue.trim()) - if (!submitValue || !submitValue.match(ONION_ADDRESS_REGEX)) { + const submitValue = getInvitationCodes(joinCommunityInput.trim()) + if (!submitValue?.length) { setLoading(false) setInputError('Please check your invitation code and try again') return diff --git a/packages/mobile/src/components/JoinCommunity/JoinCommunity.types.ts b/packages/mobile/src/components/JoinCommunity/JoinCommunity.types.ts index fb07d60ca6..3086a86790 100644 --- a/packages/mobile/src/components/JoinCommunity/JoinCommunity.types.ts +++ b/packages/mobile/src/components/JoinCommunity/JoinCommunity.types.ts @@ -1,5 +1,7 @@ +import { InvitationPair } from '@quiet/types' + export interface JoinCommunityProps { - joinCommunityAction: (address: string) => void + joinCommunityAction: (address: InvitationPair[]) => void redirectionAction: () => void networkCreated: boolean invitationCode?: string diff --git a/packages/state-manager/src/index.ts b/packages/state-manager/src/index.ts index 3f6b0da070..b247ed9833 100644 --- a/packages/state-manager/src/index.ts +++ b/packages/state-manager/src/index.ts @@ -83,7 +83,7 @@ export { formatBytes } from './utils/functions/formatBytes/formatBytes' export { sortPeers } from './utils/functions/sortPeers/sortPeers' -export { getInvitationCode } from './utils/functions/invitationCode/invitationCode' +export { getInvitationCodes } from './utils/functions/invitationCode/invitationCode' export type { Socket } from './types' diff --git a/packages/state-manager/src/sagas/communities/communities.slice.ts b/packages/state-manager/src/sagas/communities/communities.slice.ts index 41072e01d6..af67c8b8ab 100644 --- a/packages/state-manager/src/sagas/communities/communities.slice.ts +++ b/packages/state-manager/src/sagas/communities/communities.slice.ts @@ -2,6 +2,7 @@ import { createSlice, type EntityState, type PayloadAction } from '@reduxjs/tool import { StoreKeys } from '../store.keys' import { communitiesAdapter } from './communities.adapter' import { + InvitationPair, type AddOwnerCertificatePayload, type Community as CommunityType, type CreateNetworkPayload, @@ -14,6 +15,7 @@ import { export class CommunitiesState { public invitationCode: string | undefined = undefined + public invitationCodes: InvitationPair[] = [] public currentCommunity = '' public communities: EntityState = communitiesAdapter.getInitialState() } @@ -93,7 +95,13 @@ export const communitiesSlice = createSlice({ state.invitationCode = action.payload }, clearInvitationCode: state => { - state.invitationCode = undefined + state.invitationCode = '' + }, + handleInvitationCodes: (state, action: PayloadAction) => { + state.invitationCodes = action.payload + }, + clearInvitationCodes: state => { + state.invitationCodes = [] }, addOwnerCertificate: (state, action: PayloadAction) => { const { communityId, ownerCertificate } = 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 028efd328e..bb84e67e3e 100644 --- a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts +++ b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts @@ -29,7 +29,7 @@ export function* createNetworkSaga( const id = yield* call(generateId) - const registrarUrl = action.payload.registrar ? `http://${action.payload.registrar}.onion` : undefined + // const registrarUrl = action.payload.registrar ? `http://${action.payload.registrar}.onion` : undefined const payload: Community = { id, 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 31c99bf1e5..8acc98f561 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts @@ -1,14 +1,25 @@ -import { getInvitationCode } from './invitationCode' +import { getInvitationCodes } from './invitationCode' import { Site } from '@quiet/common' describe('Invitation code helper', () => { it('retrieves invitation code if url is a proper share url', () => { - const result = getInvitationCode(`https://${Site.DOMAIN}/${Site.JOIN_PAGE}#validCode`) - expect(result).toEqual('validCode') + const result = getInvitationCodes(`https://${Site.DOMAIN}/${Site.JOIN_PAGE}#peerId1=address1&peerId2=address2`) + expect(result).toEqual([ + { peerId: 'peerId1', address: 'address1' }, + { peerId: 'peerId2', address: 'address2' }, + ]) }) - it('returns passed value if url is not a proper share url', () => { - const result = getInvitationCode('validCode') - expect(result).toEqual('validCode') + it('returns empty list if code is not a proper share url nor a code', () => { + const result = getInvitationCodes('invalidCode') + expect(result).toEqual([]) + }) + + it('retrieves invitation code if url is a proper code', () => { + const result = getInvitationCodes(`peerId1=address1&peerId2=address2`) + expect(result).toEqual([ + { peerId: 'peerId1', address: 'address1' }, + { peerId: 'peerId2', address: 'address2' }, + ]) }) }) diff --git a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts index e2abc314cb..cd85cedd48 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts @@ -1,37 +1,68 @@ import { Site } from '@quiet/common' +import { InvitationPair } from '@quiet/types' -export const getInvitationCode = (codeOrUrl: string): string => { +const getInvitationPairs = (code: string) => { + const pairs = code.split('&') + const codes: InvitationPair[] = [] + for (const pair of pairs) { + const [peerId, address] = pair.split('=') + if (!peerId || !address) continue + codes.push({ + peerId: peerId, + address: address, + }) + } + return codes +} + +export const getInvitationCodes = (codeOrUrl: string): InvitationPair[] => { /** - * Extract code from invitation share url or return passed value for further validation + * Extract codes from invitation share url or return passed value for further validation */ - let code = '' + let codes: InvitationPair[] = [] + let potentialCode let validUrl: URL | null = null try { validUrl = new URL(codeOrUrl) } catch (e) { - code = codeOrUrl + // It may be just code, not URL + potentialCode = codeOrUrl } + if (validUrl && validUrl.host === Site.DOMAIN && validUrl.pathname.includes(Site.JOIN_PAGE)) { const hash = validUrl.hash + // const params = validUrl.searchParams + // TODO: I don't think handling params is needed here as we only accept url with '#' and code without url - let invitationCode: string = hash.substring(1) + // if (params) { + // // Type 'URLSearchParams' must have a '[Symbol.iterator]()' method that returns an iterator + // for (const [peerId, address] of params) { + // // TODO: basic check if peerid and address have proper format? + // if (peerId.length !== 46 || address.length !== 56) { + // console.log(`peerId '${peerId}' or address ${address} is not valid`) + // continue + // } + // codes.push({ + // peerId, + // address, + // }) + // } + // } - // Ensure backward compatibility - if (hash.includes('code=')) { - // Mix of old and new link - invitationCode = hash.substring(6) - } else if (validUrl.searchParams.has('code')) { - // Old link - invitationCode = validUrl.searchParams.get('code') || '' + if (hash) { + // Parse hash + const pairs = hash.substring(1) + codes = getInvitationPairs(pairs) } - - code = invitationCode + } else if (potentialCode) { + // Parse code just as hash value + codes = getInvitationPairs(potentialCode) } - if (!code) { - console.warn(`No invitation code. Code/url passed: ${codeOrUrl}`) + if (codes.length === 0) { + console.warn(`No invitation codes. Code/url passed: ${codeOrUrl}`) } - return code + return codes } diff --git a/packages/types/src/community.ts b/packages/types/src/community.ts index ee7c0467a2..0f2077e5c5 100644 --- a/packages/types/src/community.ts +++ b/packages/types/src/community.ts @@ -1,4 +1,5 @@ import { type HiddenService, type PeerId, type Identity } from './identity' +import { InvitationPair } from './network' export interface Community { id: string @@ -34,7 +35,7 @@ export interface NetworkData { export interface CreateNetworkPayload { ownership: CommunityOwnership name?: string - registrar?: string + peers?: InvitationPair[] } export interface ResponseCreateNetworkPayload { diff --git a/packages/types/src/network.ts b/packages/types/src/network.ts index 7795ea5b5f..beb1058ac9 100644 --- a/packages/types/src/network.ts +++ b/packages/types/src/network.ts @@ -2,3 +2,8 @@ export enum LoadingPanelType { StartingApplication = 'Starting Quiet', Joining = 'Connecting to peers', } + +export type InvitationPair = { + peerId: string + address: string +}