diff --git a/fe1-web/src/core/network/ingestion/Handler.ts b/fe1-web/src/core/network/ingestion/Handler.ts index 1d58837f41..222f768a8d 100644 --- a/fe1-web/src/core/network/ingestion/Handler.ts +++ b/fe1-web/src/core/network/ingestion/Handler.ts @@ -1,6 +1,6 @@ import { dispatch } from 'core/redux'; -import { Broadcast, JsonRpcMethod, ExtendedJsonRpcRequest } from '../jsonrpc'; +import { Broadcast, JsonRpcMethod, ExtendedJsonRpcRequest, Publish } from '../jsonrpc'; import { ActionType, MessageRegistry, ObjectType } from '../jsonrpc/messages'; import { ExtendedMessage } from './ExtendedMessage'; import { addMessages } from './MessageReducer'; @@ -61,6 +61,11 @@ export function handleExtendedRpcRequests(req: ExtendedJsonRpcRequest) { broadcastParams.channel, ), ); + } else if (req.request.method === JsonRpcMethod.PUBLISH) { + const publishParams = req.request.params as Publish; + storeMessage( + ExtendedMessage.fromMessage(publishParams.message, req.receivedFrom, publishParams.channel), + ); } else { console.warn('A request was received but it is currently unsupported:', req); } diff --git a/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts b/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts index da26c6bf41..6e4e0f3946 100644 --- a/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts +++ b/fe1-web/src/core/network/jsonrpc/messages/MessageData.ts @@ -43,6 +43,8 @@ export enum ActionType { CHALLENGE = 'challenge', FEDERATION_INIT = 'init', FEDERATION_EXPECT = 'expect', + FEDERATION_RESULT = 'result', + TOKENS_EXCHANGE = 'tokens_exchange', } /** Enumeration of all possible signatures of a message */ diff --git a/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts b/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts index e219bc1abc..342f188629 100644 --- a/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts +++ b/fe1-web/src/core/network/jsonrpc/messages/MessageRegistry.ts @@ -32,6 +32,8 @@ const { CHALLENGE, FEDERATION_INIT, FEDERATION_EXPECT, + FEDERATION_RESULT, + TOKENS_EXCHANGE, } = ActionType; const { KEYPAIR, POP_TOKEN } = SignatureType; @@ -107,6 +109,8 @@ export class MessageRegistry { [k(FEDERATION, CHALLENGE), { signature: KEYPAIR }], [k(FEDERATION, FEDERATION_INIT), { signature: KEYPAIR }], [k(FEDERATION, FEDERATION_EXPECT), { signature: KEYPAIR }], + [k(FEDERATION, FEDERATION_RESULT), { signature: KEYPAIR }], + [k(FEDERATION, TOKENS_EXCHANGE), { signature: KEYPAIR }], ]); /** diff --git a/fe1-web/src/core/network/validation/Validator.ts b/fe1-web/src/core/network/validation/Validator.ts index f8e710f265..2a165ea539 100644 --- a/fe1-web/src/core/network/validation/Validator.ts +++ b/fe1-web/src/core/network/validation/Validator.ts @@ -79,6 +79,8 @@ const schemaIds: Record> = { [ActionType.CHALLENGE]: 'dataFederationChallenge', [ActionType.FEDERATION_INIT]: 'dataFederationInit', [ActionType.FEDERATION_EXPECT]: 'dataFederationExpect', + [ActionType.FEDERATION_RESULT]: 'dataFederationResult', + [ActionType.TOKENS_EXCHANGE]: 'dataFederationTokensExchange', }, }; diff --git a/fe1-web/src/core/network/validation/schemas/dataSchemas.ts b/fe1-web/src/core/network/validation/schemas/dataSchemas.ts index 03d9dba7cd..a82b4e4aeb 100644 --- a/fe1-web/src/core/network/validation/schemas/dataSchemas.ts +++ b/fe1-web/src/core/network/validation/schemas/dataSchemas.ts @@ -36,6 +36,7 @@ import dataFederationChallengeRequest from 'protocol/query/method/message/data/d import dataFederationExpect from 'protocol/query/method/message/data/dataFederationExpect.json'; import dataFederationInit from 'protocol/query/method/message/data/dataFederationInit.json'; import dataFederationResult from 'protocol/query/method/message/data/dataFederationResult.json'; +import dataFederationTokensExchange from 'protocol/query/method/message/data/dataFederationTokensExchange.json'; /* eslint-enable import/order */ const dataSchemas = [ @@ -75,6 +76,7 @@ const dataSchemas = [ dataFederationExpect, dataFederationInit, dataFederationResult, + dataFederationTokensExchange, ]; export default dataSchemas; diff --git a/fe1-web/src/features/home/screens/ConnectConfirm.tsx b/fe1-web/src/features/home/screens/ConnectConfirm.tsx index ecde70d6da..f7b5becb4f 100644 --- a/fe1-web/src/features/home/screens/ConnectConfirm.tsx +++ b/fe1-web/src/features/home/screens/ConnectConfirm.tsx @@ -10,7 +10,7 @@ import ScreenWrapper from 'core/components/ScreenWrapper'; import { AppParamList } from 'core/navigation/typing/AppParamList'; import { ConnectParamList } from 'core/navigation/typing/ConnectParamList'; import { getNetworkManager, subscribeToChannel } from 'core/network'; -import { Hash } from 'core/objects'; +import { getFederationChannel, Hash } from 'core/objects'; import { Typography } from 'core/styles'; import containerStyles from 'core/styles/stylesheets/containerStyles'; import { FOUR_SECONDS } from 'resources/const'; @@ -63,6 +63,7 @@ const ConnectConfirm = () => { } else { // subscribe to the lao channel on the new connection await subscribeToChannel(laoId, dispatch, laoChannel, [connection]); + await subscribeToChannel(laoId, dispatch, getFederationChannel(laoId)); } navigation.navigate(STRINGS.navigation_app_lao, { diff --git a/fe1-web/src/features/home/screens/ConnectScan.tsx b/fe1-web/src/features/home/screens/ConnectScan.tsx index 6c38a2ad4d..566916a21e 100644 --- a/fe1-web/src/features/home/screens/ConnectScan.tsx +++ b/fe1-web/src/features/home/screens/ConnectScan.tsx @@ -11,7 +11,7 @@ import QrCodeScanOverlay from 'core/components/QrCodeScanOverlay'; import { AppParamList } from 'core/navigation/typing/AppParamList'; import { ConnectParamList } from 'core/navigation/typing/ConnectParamList'; import { getNetworkManager, subscribeToChannel } from 'core/network'; -import { Channel } from 'core/objects'; +import { Channel, getFederationChannel } from 'core/objects'; import { Spacing, Typography } from 'core/styles'; import { FOUR_SECONDS } from 'resources/const'; import STRINGS from 'resources/strings'; @@ -164,6 +164,7 @@ const ConnectScan = () => { ); await connectToLaoAndSubscribe(connectToLao, laoChannel); + await subscribeToChannel(connectToLao.lao, dispatch, getFederationChannel(connectToLao.lao)); isProcessingScan.current = false; setIsConnecting(false); diff --git a/fe1-web/src/features/index.ts b/fe1-web/src/features/index.ts index 24924eee97..f453fcb5fe 100644 --- a/fe1-web/src/features/index.ts +++ b/fe1-web/src/features/index.ts @@ -140,6 +140,8 @@ export function configureFeatures() { useCurrentLao: laoConfiguration.hooks.useCurrentLao, getCurrentLaoId: laoConfiguration.functions.getCurrentLaoId, getLaoOrganizerBackendPublicKey: laoConfiguration.functions.getLaoOrganizerBackendPublicKey, + getLaoById: laoConfiguration.functions.getLaoById, + getRollCallById: rollCallConfiguration.functions.getRollCallById, }); // compose features diff --git a/fe1-web/src/features/lao/objects/Lao.ts b/fe1-web/src/features/lao/objects/Lao.ts index aa54fea70a..864716dcc8 100644 --- a/fe1-web/src/features/lao/objects/Lao.ts +++ b/fe1-web/src/features/lao/objects/Lao.ts @@ -73,11 +73,11 @@ export class Lao { this.name = obj.name; this.id = obj.id; + this.last_roll_call_id = obj.last_roll_call_id; this.creation = obj.creation; this.last_modified = obj.last_modified; this.organizer = obj.organizer; this.witnesses = [...obj.witnesses]; - this.last_roll_call_id = obj.last_roll_call_id; this.last_tokenized_roll_call_id = obj.last_tokenized_roll_call_id; this.server_addresses = obj.server_addresses || []; @@ -93,11 +93,11 @@ export class Lao { return new Lao({ name: lao.name, id: Hash.fromState(lao.id), + last_roll_call_id: lao.last_roll_call_id ? Hash.fromState(lao.last_roll_call_id) : undefined, creation: Timestamp.fromState(lao.creation), last_modified: Timestamp.fromState(lao.last_modified), organizer: PublicKey.fromState(lao.organizer), witnesses: lao.witnesses.map((w) => PublicKey.fromState(w)), - last_roll_call_id: lao.last_roll_call_id ? Hash.fromState(lao.last_roll_call_id) : undefined, last_tokenized_roll_call_id: lao.last_tokenized_roll_call_id ? Hash.fromState(lao.last_tokenized_roll_call_id) : undefined, diff --git a/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx b/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx index 79f0934722..24e00f0331 100644 --- a/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx +++ b/fe1-web/src/features/linked-organizations/components/AddLinkedOrganizationModal.tsx @@ -21,7 +21,7 @@ import { expectFederation, initFederation, requestChallenge } from '../network'; import { Challenge } from '../objects/Challenge'; import { LinkedOrganization } from '../objects/LinkedOrganization'; import { makeChallengeSelector } from '../reducer'; -import { addLinkedOrganization } from '../reducer/LinkedOrganizationsReducer'; +import { addScannedLinkedOrganization } from '../reducer/LinkedOrganizationsReducer'; import ManualInputModal from './ManualInputModal'; import QRCodeModal from './QRCodeModal'; import QRCodeScannerModal from './QRCodeScannerModal'; @@ -54,6 +54,7 @@ const AddLinkedOrganizationModal = () => { const navigation = useNavigation(); const toast = useToast(); const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); + const isOrganizer = LinkedOrganizationsHooks.useIsLaoOrganizer(laoId); const lao = LinkedOrganizationsHooks.useCurrentLao(); const challengeSelector = useMemo(() => makeChallengeSelector(laoId), [laoId]); const challengeState = useSelector(challengeSelector); @@ -71,6 +72,8 @@ const AddLinkedOrganizationModal = () => { // this is needed as otherwise the camera may stay turned on const [showScanner, setShowScanner] = useState(false); + const [linkedOrganization, setLinkedOrganization] = useState(); + const onRequestChallenge = useCallback(() => { requestChallenge(laoId) .then(() => {}) @@ -100,6 +103,14 @@ const AddLinkedOrganizationModal = () => { const onFederationExpect = useCallback( (org: LinkedOrganization) => { if (challengeState) { + const linkedorg = new LinkedOrganization({ + lao_id: org.lao_id, + server_address: org.server_address, + public_key: org.public_key, + challenge: Challenge.fromState(challengeState), + }); + setLinkedOrganization(linkedorg); + dispatch(addScannedLinkedOrganization(laoId, linkedorg.toState())); expectFederation( laoId, org.lao_id, @@ -108,14 +119,10 @@ const AddLinkedOrganizationModal = () => { Challenge.fromState(challengeState), ) .then(() => { - toast.show(`Success: Expect Federation`, { - type: 'success', - placement: 'bottom', - duration: FOUR_SECONDS, - }); - dispatch(addLinkedOrganization(laoId, org.toState())); + console.log('Expect Federation successfull'); }) .catch((err) => { + console.log(err); toast.show(`Could not expect Federation, error: ${err}`, { type: 'danger', placement: 'bottom', @@ -131,14 +138,10 @@ const AddLinkedOrganizationModal = () => { (org: LinkedOrganization) => { initFederation(laoId, org.lao_id, org.server_address, org.public_key, org.challenge!) .then(() => { - toast.show(`Success: Init Federation`, { - type: 'success', - placement: 'bottom', - duration: FOUR_SECONDS, - }); - dispatch(addLinkedOrganization(laoId, org.toState())); + console.log('Init Federation successfull'); }) .catch((err) => { + console.log(err); toast.show(`Could not init Federation, error: ${err}`, { type: 'danger', placement: 'bottom', @@ -157,7 +160,8 @@ const AddLinkedOrganizationModal = () => { if (isInitiatingOrganizer) { requestChallengeAndDisplayQRCode(); setShowQRCodeModal(true); - onFederationInit(scannedLinkedOrganization); + dispatch(addScannedLinkedOrganization(laoId, scannedLinkedOrganization.toState())); + setLinkedOrganization(scannedLinkedOrganization); } else { onFederationExpect(scannedLinkedOrganization); navigation.navigate(STRINGS.navigation_linked_organizations); @@ -179,7 +183,7 @@ const AddLinkedOrganizationModal = () => { }; useEffect(() => { - if (challengeState) { + if (challengeState && isOrganizer) { const challenge = Challenge.fromState(challengeState); const jsonObj = { lao_id: laoId, @@ -192,7 +196,7 @@ const AddLinkedOrganizationModal = () => { }; setQRCodeData(JSON.stringify(jsonObj)); } - }, [challengeState, laoId, lao.organizer, lao.server_addresses]); + }, [challengeState, laoId, lao.organizer, lao.server_addresses, isOrganizer]); return ( <> @@ -267,6 +271,7 @@ const AddLinkedOrganizationModal = () => { setShowQRScannerModal(true); setShowScanner(true); } else { + onFederationInit(linkedOrganization!); navigation.navigate(STRINGS.navigation_linked_organizations); } setShowQRCodeModal(false); diff --git a/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx b/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx new file mode 100644 index 0000000000..edcefb095a --- /dev/null +++ b/fe1-web/src/features/linked-organizations/components/BroadcastLinkedOrgInfo.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useToast } from 'react-native-toast-notifications'; + +import { Hash } from 'core/objects'; +import { FOUR_SECONDS } from 'resources/const'; + +import { LinkedOrganizationsHooks } from '../hooks'; +import { tokensExchange } from '../network'; + +interface BroadcastLinkedOrgInfoProps { + linkedLaoId: Hash; +} + +const BroadcastLinkedOrgInfo: React.FC = ({ linkedLaoId }) => { + const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); + const toast = useToast(); + const fetchedLao = LinkedOrganizationsHooks.useGetLaoById(linkedLaoId); + const fetchedRollCall = LinkedOrganizationsHooks.useGetRollCallById( + fetchedLao!.last_roll_call_id!, + ); + + if (fetchedRollCall === undefined) { + toast.show(`Data Exchange failed: RollCall ID is undefined`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + return null; + } + if (fetchedRollCall.attendees === undefined) { + toast.show(`Data Exchange failed: RollCall Attendees is undefined`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + return null; + } + tokensExchange(laoId, linkedLaoId, fetchedRollCall.id, fetchedRollCall.attendees) + .then(() => { + console.log('Data Exchange successfull'); + }) + .catch((err) => { + console.log(err); + toast.show(`Could not exchange Tokens, error: ${err}`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + }); + return null; +}; + +export default BroadcastLinkedOrgInfo; diff --git a/fe1-web/src/features/linked-organizations/components/QRCodeScannerModal.tsx b/fe1-web/src/features/linked-organizations/components/QRCodeScannerModal.tsx index 93e28188e8..0c36162eaa 100644 --- a/fe1-web/src/features/linked-organizations/components/QRCodeScannerModal.tsx +++ b/fe1-web/src/features/linked-organizations/components/QRCodeScannerModal.tsx @@ -29,6 +29,14 @@ const styles = StyleSheet.create({ top: '25%', bottom: '50%', } as ViewStyle, + qrCodeMobile: { + scale: 0.65, + alignItems: 'center', + justifyContent: 'center', + opacity: 0.5, + top: '20%', + bottom: '50%', + } as ViewStyle, scannerTextItems: { top: '35%', } as ViewStyle, @@ -68,7 +76,7 @@ const QRCodeScannerModal: React.FC = ({ - + diff --git a/fe1-web/src/features/linked-organizations/components/__tests__/AddLinkedOrganizationButton.test.tsx b/fe1-web/src/features/linked-organizations/components/__tests__/AddLinkedOrganizationButton.test.tsx index 75d15452c1..3a5385f561 100644 --- a/fe1-web/src/features/linked-organizations/components/__tests__/AddLinkedOrganizationButton.test.tsx +++ b/fe1-web/src/features/linked-organizations/components/__tests__/AddLinkedOrganizationButton.test.tsx @@ -26,6 +26,8 @@ const contextValue = { useCurrentLaoId: () => mockLaoId, useIsLaoOrganizer: () => false, useCurrentLao: () => mockLaoServerAddress, + getLaoById: () => undefined, + getRollCallById: () => undefined, } as LinkedOrganizationsReactContext, }; diff --git a/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts b/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts index 2d05ac4fd0..10edae5f2b 100644 --- a/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts +++ b/fe1-web/src/features/linked-organizations/hooks/LinkedOrganizationsHooks.ts @@ -30,6 +30,17 @@ export namespace LinkedOrganizationsHooks { */ export const useCurrentLao = () => useLinkedOrganizationsContext().useCurrentLao(); + /** + * Gets the function to retrieve a lao by its id + */ + export const useGetLaoById = (laoId: Hash) => useLinkedOrganizationsContext().getLaoById(laoId); + + /** + * Gets the function to retrieve a rollcall by its id + */ + export const useGetRollCallById = (id: Hash) => + useLinkedOrganizationsContext().getRollCallById(id); + /** * Gets whether the current user is organizer of the given lao */ diff --git a/fe1-web/src/features/linked-organizations/index.ts b/fe1-web/src/features/linked-organizations/index.ts index 42fc98126c..e96effb965 100644 --- a/fe1-web/src/features/linked-organizations/index.ts +++ b/fe1-web/src/features/linked-organizations/index.ts @@ -13,7 +13,8 @@ import { challengeReducer, linkedOrganizationsReducer } from './reducer'; export function configure( configuration: LinkedOrganizationsConfiguration, ): LinkedOrganizationsInterface { - const { useCurrentLao, useCurrentLaoId, useIsLaoOrganizer } = configuration; + const { useCurrentLao, useCurrentLaoId, useIsLaoOrganizer, getLaoById, getRollCallById } = + configuration; configureNetwork(configuration); return { identifier: LINKED_ORGANIZATIONS_FEATURE_IDENTIFIER, @@ -26,6 +27,8 @@ export function configure( useCurrentLaoId: useCurrentLaoId, useIsLaoOrganizer: useIsLaoOrganizer, useCurrentLao: useCurrentLao, + getLaoById: getLaoById, + getRollCallById: getRollCallById, }, }; } diff --git a/fe1-web/src/features/linked-organizations/interface/Configuration.ts b/fe1-web/src/features/linked-organizations/interface/Configuration.ts index 2759777cc5..286ad780f3 100644 --- a/fe1-web/src/features/linked-organizations/interface/Configuration.ts +++ b/fe1-web/src/features/linked-organizations/interface/Configuration.ts @@ -52,6 +52,19 @@ export interface LinkedOrganizationsConfiguration { * @returns The current lao */ useCurrentLao: () => LinkedOrganizationsFeature.Lao; + + /** + * Gets a lao from the store by its id + * @param laoId The id of the lao + * @returns A lao or undefined if none was found + */ + getLaoById(laoId: Hash): LinkedOrganizationsFeature.Lao | undefined; + + /** + * Returns a map from laoIds to names + */ + + getRollCallById: (id: Hash) => LinkedOrganizationsFeature.RollCall | undefined; } /** @@ -59,7 +72,7 @@ export interface LinkedOrganizationsConfiguration { */ export type LinkedOrganizationsReactContext = Pick< LinkedOrganizationsConfiguration, - 'useCurrentLaoId' | 'useIsLaoOrganizer' | 'useCurrentLao' + 'useCurrentLaoId' | 'useIsLaoOrganizer' | 'useCurrentLao' | 'getLaoById' | 'getRollCallById' >; /** diff --git a/fe1-web/src/features/linked-organizations/interface/Feature.ts b/fe1-web/src/features/linked-organizations/interface/Feature.ts index a4665625f1..2c1976c9c5 100644 --- a/fe1-web/src/features/linked-organizations/interface/Feature.ts +++ b/fe1-web/src/features/linked-organizations/interface/Feature.ts @@ -1,6 +1,6 @@ import { LaoParamList } from 'core/navigation/typing/LaoParamList'; import { NavigationDrawerScreen } from 'core/navigation/typing/Screen'; -import { Hash, PublicKey } from 'core/objects'; +import { Hash, PopToken, PublicKey } from 'core/objects'; export namespace LinkedOrganizationsFeature { export interface LaoScreen extends NavigationDrawerScreen { @@ -10,5 +10,16 @@ export namespace LinkedOrganizationsFeature { id: Hash; server_addresses: string[]; organizer: PublicKey; + last_tokenized_roll_call_id?: Hash | undefined; + last_roll_call_id?: Hash | undefined; + } + + export interface RollCall { + id: Hash; + name: string; + status: number; + attendees?: PublicKey[]; + + containsToken(token: PopToken | undefined): boolean; } } diff --git a/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts b/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts index 855c14e08a..6641bd5c54 100644 --- a/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts +++ b/fe1-web/src/features/linked-organizations/network/LinkedOrgHandler.ts @@ -1,10 +1,20 @@ +import { subscribeToChannel } from 'core/network'; import { ActionType, ObjectType, ProcessableMessage } from 'core/network/jsonrpc/messages'; +import { Base64UrlData, getReactionChannel, getUserSocialChannel } from 'core/objects'; import { dispatch } from 'core/redux'; import { LinkedOrganizationsConfiguration } from '../interface'; import { Challenge } from '../objects/Challenge'; -import { setChallenge } from '../reducer'; -import { ChallengeRequest, ChallengeMessage, FederationExpect, FederationInit } from './messages'; +import { addReceivedChallenge, setChallenge } from '../reducer'; +import { addLinkedLaoId } from '../reducer/LinkedOrganizationsReducer'; +import { + ChallengeRequest, + ChallengeMessage, + FederationExpect, + FederationInit, + FederationResult, +} from './messages'; +import { TokensExchange } from './messages/TokensExchange'; /** * Handler for linked organization messages @@ -21,7 +31,6 @@ export const handleChallengeMessage = () => (msg: ProcessableMessage) => { console.warn('handleRequestChallengeMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `challenge was not processed: ${err}`; // obtain the lao id from the channel @@ -57,7 +66,6 @@ export const handleChallengeRequestMessage = console.warn('handleRequestChallengeMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `challenge/request was not processed: ${err}`; const laoId = getCurrentLaoId(); @@ -87,7 +95,6 @@ export const handleFederationInitMessage = console.warn('handleFederationInitMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `federation/init was not processed: ${err}`; const laoId = getCurrentLaoId(); @@ -123,7 +130,6 @@ export const handleFederationExpectMessage = console.warn('handleFederationExpectMessage was called to process an unsupported message'); return false; } - const makeErr = (err: string) => `federation/expect was not processed: ${err}`; const laoId = getCurrentLaoId(); @@ -145,3 +151,120 @@ export const handleFederationExpectMessage = } return false; }; + +/** + * Handles an federationResult message. + */ +export const handleFederationResultMessage = + (getCurrentLaoId: LinkedOrganizationsConfiguration['getCurrentLaoId']) => + (msg: ProcessableMessage) => { + if ( + msg.messageData.object !== ObjectType.FEDERATION || + msg.messageData.action !== ActionType.FEDERATION_RESULT + ) { + console.warn('handleFederationResultMessage was called to process an unsupported message'); + return false; + } + const makeErr = (err: string) => `federation/result was not processed: ${err}`; + + const laoId = getCurrentLaoId(); + if (!laoId) { + console.warn(makeErr('no Lao is currently active')); + return false; + } + + if (msg.messageData instanceof FederationResult) { + const federationResult = msg.messageData as FederationResult; + try { + if ( + federationResult.status && + federationResult.challenge && + (federationResult.reason || federationResult.public_key) + ) { + const b64urldata = new Base64UrlData(federationResult.challenge.data.toString()); + const js = JSON.parse(b64urldata.decode()); + const challengeMessage = ChallengeMessage.fromJson(js); + const challenge = new Challenge({ + value: challengeMessage.value, + valid_until: challengeMessage.valid_until, + }); + dispatch(addReceivedChallenge(laoId, challenge.toState(), federationResult.public_key)); + } + } catch (e) { + console.log(e); + return false; + } + + return true; + } + return false; + }; + +/** + * Handles an tokensExchange message. + */ +export const handleTokensExchangeMessage = + (getCurrentLaoId: LinkedOrganizationsConfiguration['getCurrentLaoId']) => + (msg: ProcessableMessage) => { + if ( + msg.messageData.object !== ObjectType.FEDERATION || + msg.messageData.action !== ActionType.TOKENS_EXCHANGE + ) { + console.warn('handleTokensExchangeMessage was called to process an unsupported message'); + return false; + } + const makeErr = (err: string) => `federation/tokensExchange was not processed: ${err}`; + + const laoId = getCurrentLaoId(); + if (!laoId) { + console.warn(makeErr('no Lao is currently active')); + return false; + } + + if (msg.messageData instanceof TokensExchange) { + const tokensExchange = msg.messageData as TokensExchange; + if ( + tokensExchange.lao_id && + tokensExchange.roll_call_id && + tokensExchange.tokens && + tokensExchange.timestamp + ) { + dispatch(addLinkedLaoId(laoId, tokensExchange.lao_id)); + const subscribeChannels = async (): Promise => { + const subscribePromises = tokensExchange.tokens.map(async (attendee) => { + try { + await subscribeToChannel( + tokensExchange.lao_id, + dispatch, + getUserSocialChannel(tokensExchange.lao_id, attendee), + ); + } catch (err) { + console.error( + `Could not subscribe to social channel of attendee with public key '${attendee}', error:`, + err, + ); + } + }); + + try { + await Promise.all(subscribePromises); + } catch (err) { + console.error('Error subscribing to one or more social channels:', err); + } + + try { + await subscribeToChannel( + tokensExchange.lao_id, + dispatch, + getReactionChannel(tokensExchange.lao_id), + ); + } catch (err) { + console.error('Could not subscribe to reaction channel, error:', err); + } + }; + subscribeChannels(); + return true; + } + } + return false; + }; diff --git a/fe1-web/src/features/linked-organizations/network/LinkedOrgMessageApi.ts b/fe1-web/src/features/linked-organizations/network/LinkedOrgMessageApi.ts index 9aac213ca5..3bb2d11d1e 100644 --- a/fe1-web/src/features/linked-organizations/network/LinkedOrgMessageApi.ts +++ b/fe1-web/src/features/linked-organizations/network/LinkedOrgMessageApi.ts @@ -4,6 +4,7 @@ import { getFederationChannel, Hash, PublicKey, Timestamp } from 'core/objects'; import { Challenge } from '../objects/Challenge'; import { ChallengeRequest, ChallengeMessage, FederationExpect, FederationInit } from './messages'; +import { TokensExchange } from './messages/TokensExchange'; /** * Contains all functions to send social media related messages. @@ -74,3 +75,23 @@ export async function expectFederation( }); return publish(channel, message); } + +/** + * Sends a query to the server to exchange tokens + * + */ +export async function tokensExchange( + lao_id: Hash, + linked_lao_id: Hash, + roll_call_id: Hash, + tokens: PublicKey[], +): Promise { + const message = new TokensExchange({ + lao_id: linked_lao_id, + roll_call_id: roll_call_id, + tokens: tokens, + timestamp: Timestamp.EpochNow(), + }); + const channel = getFederationChannel(lao_id); + return publish(channel, message); +} diff --git a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts index ab083970c3..3a42ec166f 100644 --- a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts +++ b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgHandler.test.ts @@ -1,7 +1,13 @@ import 'jest-extended'; import '__tests__/utils/matchers'; -import { mockAddress, mockKeyPair, mockLaoId, mockLaoServerAddress } from '__tests__/utils'; +import { + mockAddress, + mockKeyPair, + mockLaoId, + mockLaoId2, + mockLaoServerAddress, +} from '__tests__/utils'; import { ActionType, Message, ObjectType, ProcessableMessage } from 'core/network/jsonrpc/messages'; import { Base64UrlData, @@ -14,20 +20,29 @@ import { import { dispatch } from 'core/redux'; import { Challenge } from 'features/linked-organizations/objects/Challenge'; import { setChallenge } from 'features/linked-organizations/reducer'; +import { mockRollCall } from 'features/rollCall/__tests__/utils'; import { handleChallengeMessage, handleChallengeRequestMessage, handleFederationExpectMessage, handleFederationInitMessage, + handleTokensExchangeMessage, } from '../LinkedOrgHandler'; -import { ChallengeRequest, ChallengeMessage, FederationExpect, FederationInit } from '../messages'; +import { + ChallengeRequest, + ChallengeMessage, + FederationExpect, + FederationInit, + FederationResult, +} from '../messages'; +import { TokensExchange } from '../messages/TokensExchange'; jest.mock('core/network/jsonrpc/messages/Message', () => { return { - Message: jest.fn().mockImplementation(() => { + Message: jest.fn().mockImplementation((x) => { return { - buildMessageData: jest.fn((input) => JSON.stringify(input)), + buildMessageData: jest.fn(() => JSON.parse(JSON.stringify(x))), }; }), }; @@ -81,6 +96,13 @@ const mockMessageData = { witness_signatures: [], }; +const mockTokensExchange = new TokensExchange({ + lao_id: mockLaoId2, + roll_call_id: mockRollCall.id, + tokens: mockRollCall.attendees, + timestamp: TIMESTAMP, +}); + const getCurrentLaoId = () => mockLaoId; jest.mock('core/redux', () => { @@ -318,3 +340,109 @@ describe('handleFederationExpectMessage', () => { ).toBeTrue(); }); }); + +describe('handleFederationResultMessage', () => { + it('should return false if the object type is wrong', () => { + expect( + handleChallengeMessage()({ + ...mockMessageData, + messageData: { + object: ObjectType.MEETING, + action: ActionType.FEDERATION_RESULT, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if the action type is wrong', () => { + expect( + handleChallengeMessage()({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.ADD, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if there is an issue with the message data', () => { + expect( + handleChallengeMessage()({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.FEDERATION_RESULT, + challenge: undefined as unknown as Message, + status: undefined as unknown as string, + public_key: undefined as unknown as PublicKey, + } as FederationResult, + }), + ).toBeFalse(); + }); + it('should return false if there is both a public key and a reason in the message data', () => { + expect( + handleChallengeMessage()({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.FEDERATION_RESULT, + challenge: mockChallengMessageData, + status: 'success', + reason: 'error', + public_key: mockSender, + } as FederationResult, + }), + ).toBeFalse(); + }); +}); + +describe('handleTokensExchangeMessage', () => { + it('should return false if the object type is wrong', () => { + expect( + handleTokensExchangeMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.MEETING, + action: ActionType.TOKENS_EXCHANGE, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if the action type is wrong', () => { + expect( + handleTokensExchangeMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.ADD, + }, + } as ProcessableMessage), + ).toBeFalse(); + }); + + it('should return false if there is an issue with the message data', () => { + expect( + handleTokensExchangeMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: { + object: ObjectType.FEDERATION, + action: ActionType.TOKENS_EXCHANGE, + lao_id: undefined as unknown as Hash, + roll_call_id: undefined as unknown as Hash, + tokens: undefined as unknown as PublicKey[], + timestamp: undefined as unknown as Timestamp, + } as TokensExchange, + }), + ).toBeFalse(); + }); + it('should return false if there is both a public key and a reason in the message data', () => { + expect( + handleTokensExchangeMessage(getCurrentLaoId)({ + ...mockMessageData, + messageData: mockTokensExchange, + }), + ).toBeTrue(); + }); +}); diff --git a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts index 49d85c766e..276f8bac4d 100644 --- a/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts +++ b/fe1-web/src/features/linked-organizations/network/__tests__/LinkedOrgMessageApi.test.ts @@ -14,11 +14,13 @@ import { publish as mockPublish } from 'core/network/JsonRpcApi'; import { Hash, Timestamp } from 'core/objects'; import { OpenedLaoStore } from 'features/lao/store'; import { Challenge, ChallengeState } from 'features/linked-organizations/objects/Challenge'; +import { mockRollCall } from 'features/rollCall/__tests__/utils'; import * as msApi from '../LinkedOrgMessageApi'; import { ChallengeRequest } from '../messages'; import { FederationExpect } from '../messages/FederationExpect'; import { FederationInit } from '../messages/FederationInit'; +import { TokensExchange } from '../messages/TokensExchange'; jest.mock('core/network/JsonRpcApi', () => { return { @@ -67,6 +69,18 @@ const checkDataFederationExpect = (obj: MessageData) => { expect(data.challenge).toBeInstanceOf(Message); }; +const checkDataTokensExchange = (obj: MessageData) => { + expect(obj.object).toBe(ObjectType.FEDERATION); + expect(obj.action).toBe(ActionType.TOKENS_EXCHANGE); + + const data: TokensExchange = obj as TokensExchange; + expect(data).toBeObject(); + expect(data.lao_id).toBeBase64Url(); + expect(data.roll_call_id).toBeBase64Url(); + expect(data.tokens).toBeArray(); + expect(data.timestamp).toBeNumberObject(); +}; + beforeAll(configureTestFeatures); beforeEach(() => { @@ -87,7 +101,7 @@ describe('LinkedOrgMessageApi', () => { it('should create the correct request for initFederation', async () => { const challengeState: ChallengeState = { value: VALID_HASH_VALUE.toState(), - valid_until: VALID_TIMESTAMP, + valid_until: VALID_TIMESTAMP.valueOf(), }; const challenge = Challenge.fromState(challengeState); await msApi.initFederation( @@ -107,7 +121,7 @@ describe('LinkedOrgMessageApi', () => { it('should create the correct request for expectFederation', async () => { const challengeState: ChallengeState = { value: VALID_HASH_VALUE.toState(), - valid_until: VALID_TIMESTAMP, + valid_until: VALID_TIMESTAMP.valueOf(), }; const challenge = Challenge.fromState(challengeState); await msApi.expectFederation( @@ -123,4 +137,24 @@ describe('LinkedOrgMessageApi', () => { expect(channel).toBe(`/root/${mockLaoId2.valueOf()}/federation`); checkDataFederationExpect(msgData); }); + + it('should create the correct request for tokenExchange', async () => { + const mockTokensExchange = new TokensExchange({ + lao_id: mockLaoId2, + roll_call_id: mockRollCall.id, + tokens: mockRollCall.attendees, + timestamp: VALID_TIMESTAMP, + }); + await msApi.tokensExchange( + mockLaoId, + mockTokensExchange.lao_id, + mockTokensExchange.roll_call_id, + mockTokensExchange.tokens, + ); + + expect(publishMock).toBeCalledTimes(1); + const [channel, msgData] = publishMock.mock.calls[0]; + expect(channel).toBe(`/root/${mockLaoId.valueOf()}/federation`); + checkDataTokensExchange(msgData); + }); }); diff --git a/fe1-web/src/features/linked-organizations/network/index.ts b/fe1-web/src/features/linked-organizations/network/index.ts index 27bcee8af2..c48bcfb373 100644 --- a/fe1-web/src/features/linked-organizations/network/index.ts +++ b/fe1-web/src/features/linked-organizations/network/index.ts @@ -6,8 +6,17 @@ import { handleChallengeRequestMessage, handleFederationExpectMessage, handleFederationInitMessage, + handleFederationResultMessage, + handleTokensExchangeMessage, } from './LinkedOrgHandler'; -import { ChallengeRequest, ChallengeMessage, FederationExpect, FederationInit } from './messages'; +import { + ChallengeRequest, + ChallengeMessage, + FederationExpect, + FederationInit, + FederationResult, +} from './messages'; +import { TokensExchange } from './messages/TokensExchange'; export * from './LinkedOrgMessageApi'; @@ -41,4 +50,16 @@ export function configureNetwork(configuration: LinkedOrganizationsConfiguration handleFederationExpectMessage(configuration.getCurrentLaoId), FederationExpect.fromJson, ); + configuration.messageRegistry.add( + ObjectType.FEDERATION, + ActionType.FEDERATION_RESULT, + handleFederationResultMessage(configuration.getCurrentLaoId), + FederationResult.fromJson, + ); + configuration.messageRegistry.add( + ObjectType.FEDERATION, + ActionType.TOKENS_EXCHANGE, + handleTokensExchangeMessage(configuration.getCurrentLaoId), + TokensExchange.fromJson, + ); } diff --git a/fe1-web/src/features/linked-organizations/network/messages/ChallengeMessage.ts b/fe1-web/src/features/linked-organizations/network/messages/ChallengeMessage.ts index 37b17865c5..a720fe4c00 100644 --- a/fe1-web/src/features/linked-organizations/network/messages/ChallengeMessage.ts +++ b/fe1-web/src/features/linked-organizations/network/messages/ChallengeMessage.ts @@ -34,8 +34,8 @@ export class ChallengeMessage implements MessageData { throw new ProtocolError(`Invalid challenge\n\n${errors}`); } return new ChallengeMessage({ - value: obj.value, - valid_until: obj.valid_until, + value: new Hash(obj.value), + valid_until: new Timestamp(obj.valid_until), }); } } diff --git a/fe1-web/src/features/linked-organizations/network/messages/FederationResult.ts b/fe1-web/src/features/linked-organizations/network/messages/FederationResult.ts new file mode 100644 index 0000000000..685b67a190 --- /dev/null +++ b/fe1-web/src/features/linked-organizations/network/messages/FederationResult.ts @@ -0,0 +1,58 @@ +import { ActionType, Message, MessageData, ObjectType } from 'core/network/jsonrpc/messages'; +import { validateDataObject } from 'core/network/validation'; +import { ProtocolError, PublicKey } from 'core/objects'; + +/** Result received for the Federation Authentication */ +export class FederationResult implements MessageData { + public readonly object: ObjectType = ObjectType.FEDERATION; + + public readonly action: ActionType = ActionType.FEDERATION_RESULT; + + public readonly reason?: String; + + public readonly public_key?: PublicKey; + + public readonly status: String; + + public readonly challenge: Message; + + constructor(msg: Partial) { + if (!msg.status) { + throw new ProtocolError("Undefined 'status' parameter encountered during 'FederationResult'"); + } + if (!msg.challenge) { + throw new ProtocolError( + "Undefined 'challenge' parameter encountered during 'FederationResult'", + ); + } + + if (!msg.reason && !msg.public_key) { + throw new ProtocolError( + "Undefined 'reason' or 'public_key' parameter encountered during 'FederationResult'", + ); + } + + this.status = msg.status; + this.reason = msg.reason; + this.public_key = msg.public_key; + this.challenge = msg.challenge; + } + + /** + * Creates an FederationResult object from a given object + * @param obj + */ + public static fromJson(obj: any): FederationResult { + const { errors } = validateDataObject(ObjectType.FEDERATION, ActionType.FEDERATION_RESULT, obj); + if (errors !== null) { + throw new ProtocolError(`Invalid federation result\n\n${errors}`); + } + + return new FederationResult({ + reason: obj.reason, + challenge: obj.challenge, + status: obj.status, + public_key: obj.public_key, + }); + } +} diff --git a/fe1-web/src/features/linked-organizations/network/messages/TokensExchange.ts b/fe1-web/src/features/linked-organizations/network/messages/TokensExchange.ts new file mode 100644 index 0000000000..ec533e8b96 --- /dev/null +++ b/fe1-web/src/features/linked-organizations/network/messages/TokensExchange.ts @@ -0,0 +1,61 @@ +import { ActionType, MessageData, ObjectType } from 'core/network/jsonrpc/messages'; +import { validateDataObject } from 'core/network/validation'; +import { Hash, ProtocolError, PublicKey, Timestamp } from 'core/objects'; + +/** Data sent to exchange tokens */ +export class TokensExchange implements MessageData { + public readonly object: ObjectType = ObjectType.FEDERATION; + + public readonly action: ActionType = ActionType.TOKENS_EXCHANGE; + + public readonly lao_id: Hash; + + public readonly roll_call_id: Hash; + + public readonly tokens: PublicKey[]; + + public readonly timestamp: Timestamp; + + constructor(msg: Partial) { + if (!msg.lao_id) { + throw new ProtocolError("Undefined 'lao_id' parameter encountered during 'TokensExchange'"); + } + if (!msg.roll_call_id) { + throw new ProtocolError( + "Undefined 'roll_call_id' parameter encountered during 'TokensExchange'", + ); + } + if (!msg.tokens || msg.tokens.length === 0) { + throw new ProtocolError( + "Undefined or empty 'tokens' parameter encountered during 'TokensExchange'", + ); + } + if (!msg.timestamp) { + throw new ProtocolError( + "Undefined 'timestamp' parameter encountered during 'TokensExchange'", + ); + } + this.lao_id = msg.lao_id; + this.roll_call_id = msg.roll_call_id; + this.tokens = msg.tokens; + this.timestamp = msg.timestamp; + } + + /** + * Creates an TokensExchange object from a given object + * @param obj + */ + public static fromJson(obj: any): TokensExchange { + const { errors } = validateDataObject(ObjectType.FEDERATION, ActionType.TOKENS_EXCHANGE, obj); + if (errors !== null) { + throw new ProtocolError(`Invalid tokens exchange\n\n${errors}`); + } + + return new TokensExchange({ + lao_id: obj.lao_id, + roll_call_id: obj.roll_call_id, + tokens: obj.tokens, + timestamp: obj.timestamp, + }); + } +} diff --git a/fe1-web/src/features/linked-organizations/network/messages/index.ts b/fe1-web/src/features/linked-organizations/network/messages/index.ts index 621a91753d..19226bc8a5 100644 --- a/fe1-web/src/features/linked-organizations/network/messages/index.ts +++ b/fe1-web/src/features/linked-organizations/network/messages/index.ts @@ -2,3 +2,4 @@ export * from './ChallengeRequest'; export * from './ChallengeMessage'; export * from './FederationExpect'; export * from './FederationInit'; +export * from './FederationResult'; diff --git a/fe1-web/src/features/linked-organizations/objects/Challenge.ts b/fe1-web/src/features/linked-organizations/objects/Challenge.ts index 7996ff827a..1e00ea1002 100644 --- a/fe1-web/src/features/linked-organizations/objects/Challenge.ts +++ b/fe1-web/src/features/linked-organizations/objects/Challenge.ts @@ -3,7 +3,7 @@ import { OmitMethods } from 'core/types'; export interface ChallengeState { value: HashState; - valid_until: Timestamp; + valid_until: number; } export class Challenge { @@ -32,21 +32,21 @@ export class Challenge { public toState(): ChallengeState { return { value: this.value.toState(), - valid_until: this.valid_until, + valid_until: this.valid_until.valueOf(), }; } public static fromState(challengeState: ChallengeState): Challenge { return new Challenge({ value: Hash.fromState(challengeState.value), - valid_until: challengeState.valid_until, + valid_until: new Timestamp(challengeState.valid_until), }); } public static fromJson(obj: any): Challenge { return new Challenge({ value: new Hash(obj.value), - valid_until: obj.valid_until, + valid_until: new Timestamp(obj.valid_until), }); } diff --git a/fe1-web/src/features/linked-organizations/objects/__tests__/Challenge.test.ts b/fe1-web/src/features/linked-organizations/objects/__tests__/Challenge.test.ts index 557291f369..2121a0200a 100644 --- a/fe1-web/src/features/linked-organizations/objects/__tests__/Challenge.test.ts +++ b/fe1-web/src/features/linked-organizations/objects/__tests__/Challenge.test.ts @@ -13,7 +13,7 @@ describe('Challenge object', () => { it('does a state round trip correctly', () => { const challengeState: ChallengeState = { value: VALID_HASH_VALUE.toState(), - valid_until: VALID_TIMESTAMP, + valid_until: VALID_TIMESTAMP.valueOf(), }; const challenge = Challenge.fromState(challengeState); expect(challenge.toState()).toStrictEqual(challengeState); diff --git a/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts b/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts index b45ac0728f..d0f535eed5 100644 --- a/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts +++ b/fe1-web/src/features/linked-organizations/reducer/ChallengeReducer.ts @@ -5,7 +5,7 @@ /* eslint-disable no-param-reassign */ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Hash } from 'core/objects'; +import { Hash, PublicKey } from 'core/objects'; import { ChallengeState } from '../objects/Challenge'; @@ -13,10 +13,12 @@ export const CHALLENGE_REDUCER_PATH = 'challenge'; export interface ChallengeReducerState { byLaoId: Record; + recvChallenges: Record; } const initialState: ChallengeReducerState = { byLaoId: {}, + recvChallenges: {}, }; const challengeSlice = createSlice({ @@ -37,10 +39,69 @@ const challengeSlice = createSlice({ state.byLaoId[laoId] = challenge; }, }, + addReceivedChallenge: { + prepare(laoId: Hash, challenge: ChallengeState, publicKey?: PublicKey) { + return { + payload: { + laoId: laoId.valueOf(), + challenge: challenge, + publicKey: publicKey, + }, + }; + }, + reducer( + state, + action: PayloadAction<{ laoId: string; challenge: ChallengeState; publicKey?: PublicKey }>, + ) { + const { laoId, challenge, publicKey } = action.payload; + if (state.recvChallenges[laoId] === undefined) { + state.recvChallenges[laoId] = []; + } + if ( + state.recvChallenges[laoId].find( + ([challenge1]) => + challenge1.value.valueOf() === challenge.value.valueOf() && + challenge1.valid_until.valueOf() === challenge.valid_until.valueOf(), + ) + ) { + return; + } + state.recvChallenges[laoId].push([challenge, publicKey]); + }, + }, + removeReceivedChallenge: { + prepare(laoId: Hash, challenge: ChallengeState, publicKey?: PublicKey) { + return { + payload: { + laoId: laoId.valueOf(), + challenge: challenge, + publicKey: publicKey, + }, + }; + }, + reducer( + state, + action: PayloadAction<{ laoId: string; challenge: ChallengeState; publicKey?: PublicKey }>, + ) { + const { laoId, challenge } = action.payload; + if (state.recvChallenges[laoId] === undefined) { + return; + } + + state.recvChallenges[laoId] = state.recvChallenges[laoId].filter( + ([challenge1]) => + !( + challenge1.valid_until.valueOf() === challenge.valid_until.valueOf() && + challenge1.value.valueOf() === challenge.value.valueOf() + ), + ); + }, + }, }, }); -export const { setChallenge } = challengeSlice.actions; +export const { setChallenge, addReceivedChallenge, removeReceivedChallenge } = + challengeSlice.actions; export const getChallengeState = (state: any): ChallengeReducerState => state[CHALLENGE_REDUCER_PATH]; @@ -65,6 +126,26 @@ export const makeChallengeSelector = (laoId: Hash) => { ); }; +/** + * Retrives all received challenges from a lao + * @param laoId The id of the lao + * @returns Array of challenges and publickeys + */ +export const makeChallengeReceveidSelector = (laoId: Hash) => { + return createSelector( + // First input: a map containing all challenges + (state: any) => getChallengeState(state), + // Selector: returns the challenge for a specific lao + (challengeState: ChallengeReducerState): [ChallengeState, PublicKey?][] | undefined => { + const serializedLaoId = laoId.valueOf(); + if (!challengeState) { + return undefined; + } + return challengeState.recvChallenges[serializedLaoId]; + }, + ); +}; + export const challengeReduce = challengeSlice.reducer; export default { diff --git a/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts b/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts index 862affafc1..502116e754 100644 --- a/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts +++ b/fe1-web/src/features/linked-organizations/reducer/LinkedOrganizationsReducer.ts @@ -17,6 +17,7 @@ export interface LinkedOrganizationReducerState { byLinkedLaoId: Record; allLaoIds: string[]; allLaos: LinkedOrganizationState[]; + allScannedLaos: LinkedOrganizationState[]; }; }; } @@ -50,6 +51,7 @@ const linkedOrganizationSlice = createSlice({ allLaoIds: [], byLinkedLaoId: {}, allLaos: [], + allScannedLaos: [], }; } @@ -64,10 +66,102 @@ const linkedOrganizationSlice = createSlice({ state.byLaoId[laoId].byLinkedLaoId[linkedOrganization.lao_id] = linkedOrganization; }, }, + addLinkedLaoId: { + prepare(laoId: Hash, linkedLaoId: Hash) { + return { + payload: { + laoId: laoId.valueOf(), + linkedLaoId: linkedLaoId.valueOf(), + }, + }; + }, + reducer(state, action: PayloadAction<{ laoId: string; linkedLaoId: string }>) { + const { laoId, linkedLaoId } = action.payload; + + if (state.byLaoId[laoId] === undefined) { + state.byLaoId[laoId] = { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [], + }; + } + + if ( + !state.byLaoId[laoId].allLaoIds.includes(linkedLaoId.valueOf()) && + linkedLaoId.valueOf() !== laoId.valueOf() + ) { + state.byLaoId[laoId].allLaoIds.push(linkedLaoId); + } + }, + }, + addScannedLinkedOrganization: { + prepare(laoId: Hash, linkedOrganization: LinkedOrganizationState) { + return { + payload: { + laoId: laoId.valueOf(), + linkedOrganization: linkedOrganization, + }, + }; + }, + reducer( + state, + action: PayloadAction<{ laoId: string; linkedOrganization: LinkedOrganizationState }>, + ) { + const { laoId, linkedOrganization } = action.payload; + + if (state.byLaoId[laoId] === undefined) { + state.byLaoId[laoId] = { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [], + }; + } + + if (state.byLaoId[laoId].allLaoIds.includes(linkedOrganization.lao_id.valueOf())) { + throw new Error( + `Tried to store organization with lao id ${linkedOrganization.lao_id} but there already exists one with the same lao id`, + ); + } + + state.byLaoId[laoId].allScannedLaos.push(linkedOrganization); + }, + }, + removeScannedLinkedOrganization: { + prepare(laoId: Hash, linkedLaoId: string) { + return { + payload: { + laoId: laoId.valueOf(), + linkedLaoId: linkedLaoId, + }, + }; + }, + reducer(state, action: PayloadAction<{ laoId: string; linkedLaoId: string }>) { + const { laoId, linkedLaoId } = action.payload; + + if (state.byLaoId[laoId] === undefined) { + state.byLaoId[laoId] = { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [], + }; + } + state.byLaoId[laoId].allScannedLaos = state.byLaoId[laoId].allScannedLaos.filter( + (org) => org.lao_id !== linkedLaoId, + ); + }, + }, }, }); -export const { addLinkedOrganization } = linkedOrganizationSlice.actions; +export const { + addLinkedOrganization, + addScannedLinkedOrganization, + removeScannedLinkedOrganization, + addLinkedLaoId, +} = linkedOrganizationSlice.actions; export const getLinkedOrganizationState = (state: any): LinkedOrganizationReducerState => state[LINKEDORGANIZATIONS_REDUCER_PATH]; @@ -93,11 +187,34 @@ export const makeSingleLinkedOrganizationSelector = (laoId: Hash, linked_lao_id: }; /** - * Retrives all linked organization state by lao id + * Retrives all linked organization ids by lao id * @param laoId The id of the lao - * @returns A list of linked organization state + * @returns A list of linked organization ids */ export const makeLinkedOrganizationSelector = (laoId: Hash) => { + return createSelector( + // First input: a map containing all linked organization ids + (state: any) => getLinkedOrganizationState(state), + // Selector: returns the linked organization ids for a specific lao and linked_lao_id + (linkedOrganizationState: LinkedOrganizationReducerState): string[] | [] => { + const serializedLaoId = laoId.valueOf(); + if (!linkedOrganizationState) { + return []; + } + if (!linkedOrganizationState.byLaoId[serializedLaoId]) { + return []; + } + return linkedOrganizationState.byLaoId[serializedLaoId].allLaoIds; + }, + ); +}; + +/** + * Retrives all scanned linked organization state by lao id + * @param laoId The id of the lao + * @returns A list of linked organization state + */ +export const makeScannedLinkedOrganizationSelector = (laoId: Hash) => { return createSelector( // First input: a map containing all linked organizations (state: any) => getLinkedOrganizationState(state), @@ -110,7 +227,7 @@ export const makeLinkedOrganizationSelector = (laoId: Hash) => { if (!linkedOrganizationState.byLaoId[serializedLaoId]) { return []; } - return linkedOrganizationState.byLaoId[serializedLaoId].allLaos; + return linkedOrganizationState.byLaoId[serializedLaoId].allScannedLaos; }, ); }; diff --git a/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts b/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts index 7dbd20b9d8..4cf7f8c918 100644 --- a/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts +++ b/fe1-web/src/features/linked-organizations/reducer/__tests__/ChallengeReducer.test.ts @@ -1,7 +1,7 @@ import { describe } from '@jest/globals'; import { AnyAction } from 'redux'; -import { mockLaoId, mockLaoId2, serializedMockLaoId } from '__tests__/utils'; +import { mockLao, mockLaoId, mockLaoId2, serializedMockLaoId } from '__tests__/utils'; import { Hash, Timestamp } from 'core/objects'; import { Challenge, ChallengeState } from 'features/linked-organizations/objects/Challenge'; import { DAY_IN_SECONDS } from 'resources/const'; @@ -12,6 +12,9 @@ import { challengeReduce, ChallengeReducerState, makeChallengeSelector, + addReceivedChallenge, + removeReceivedChallenge, + makeChallengeReceveidSelector, } from '../ChallengeReducer'; const mockChallenge: Challenge = new Challenge({ @@ -26,6 +29,7 @@ describe('ChallengeReducer', () => { it('returns a valid initial state', () => { expect(challengeReduce(undefined, {} as AnyAction)).toEqual({ byLaoId: {}, + recvChallenges: {}, } as ChallengeReducerState); }); }); @@ -35,12 +39,55 @@ describe('ChallengeReducer', () => { const newState = challengeReduce( { byLaoId: {}, + recvChallenges: {}, } as ChallengeReducerState, setChallenge(mockLaoId, mockChallengeState), ); expect(newState.byLaoId[serializedMockLaoId]).toEqual(mockChallengeState); }); }); + + describe('addReceivedChallenge', () => { + it('adds new received challenge (from fed-result) to the state', () => { + const newState = challengeReduce( + { + byLaoId: {}, + recvChallenges: {}, + } as ChallengeReducerState, + addReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), + ); + expect( + newState.recvChallenges[mockLaoId.valueOf()]?.find( + ([challenge]) => + challenge.value === mockChallengeState.value && + challenge.valid_until === mockChallengeState.valid_until, + ), + ).toBeTruthy(); + }); + }); + + describe('removeReceivedChallenge', () => { + it('removes new received challenge (from fed-result) from the state', () => { + let newState = challengeReduce( + { + byLaoId: {}, + recvChallenges: {}, + } as ChallengeReducerState, + addReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), + ); + newState = challengeReduce( + newState, + removeReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), + ); + expect( + newState.recvChallenges[mockLaoId.valueOf()]?.find( + ([challenge]) => + challenge.value === mockChallengeState.value && + challenge.valid_until === mockChallengeState.valid_until, + ), + ).toBeUndefined(); + }); + }); }); describe('makeChallengeSelector', () => { @@ -48,6 +95,7 @@ describe('makeChallengeSelector', () => { const newState = challengeReduce( { byLaoId: {}, + recvChallenges: {}, } as ChallengeReducerState, setChallenge(mockLaoId, mockChallengeState), ); @@ -65,6 +113,7 @@ describe('makeChallengeSelector', () => { const newState = challengeReduce( { byLaoId: {}, + recvChallenges: {}, } as ChallengeReducerState, setChallenge(mockLaoId, mockChallengeState), ); @@ -78,3 +127,55 @@ describe('makeChallengeSelector', () => { ).toBeUndefined(); }); }); + +describe('makeChallengeReceveidSelector', () => { + it('returns the correct challenge(s)', () => { + const newState = challengeReduce( + { + byLaoId: {}, + recvChallenges: {}, + } as ChallengeReducerState, + addReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), + ); + expect( + newState.recvChallenges[mockLaoId.valueOf()]?.find( + ([challenge]) => + challenge.value.valueOf() === mockChallengeState.value.valueOf() && + challenge.valid_until.valueOf() === mockChallengeState.valid_until.valueOf(), + ), + ).toBeTruthy(); + expect( + makeChallengeReceveidSelector(mockLaoId)({ + [CHALLENGE_REDUCER_PATH]: { + byLaoId: {}, + recvChallenges: { [serializedMockLaoId]: [[mockChallengeState, mockLao.organizer]] }, + } as ChallengeReducerState, + }), + ).toEqual([[mockChallengeState, mockLao.organizer]]); + }); + + it('returns undefined if the laoId is not in the store', () => { + const newState = challengeReduce( + { + byLaoId: {}, + recvChallenges: {}, + } as ChallengeReducerState, + addReceivedChallenge(mockLaoId, mockChallengeState, mockLao.organizer), + ); + expect( + newState.recvChallenges[mockLaoId.valueOf()]?.find( + ([challenge]) => + challenge.value.valueOf() === mockChallengeState.value.valueOf() && + challenge.valid_until.valueOf() === mockChallengeState.valid_until.valueOf(), + ), + ).toBeTruthy(); + expect( + makeChallengeReceveidSelector(mockLaoId2)({ + [CHALLENGE_REDUCER_PATH]: { + byLaoId: {}, + recvChallenges: { [serializedMockLaoId]: [[mockChallengeState, mockLao.organizer]] }, + } as ChallengeReducerState, + }), + ).toBeUndefined(); + }); +}); diff --git a/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts b/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts index 4e0467c93f..036b9ea516 100644 --- a/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts +++ b/fe1-web/src/features/linked-organizations/reducer/__tests__/LinkedOrganizationsReducer.test.ts @@ -8,11 +8,14 @@ import { LinkedOrganizationState } from 'features/linked-organizations/objects/L import { addLinkedOrganization, + addScannedLinkedOrganization, LinkedOrganizationReducerState, LINKEDORGANIZATIONS_REDUCER_PATH, linkedOrganizationsReduce, makeLinkedOrganizationSelector, + makeScannedLinkedOrganizationSelector, makeSingleLinkedOrganizationSelector, + removeScannedLinkedOrganization, } from '../LinkedOrganizationsReducer'; const mockChallenge: Challenge = new Challenge({ @@ -73,6 +76,55 @@ describe('LinkedOrganizationReducer', () => { ).toThrow(); }); }); + + describe('addScannedLinkedOrganization', () => { + it('adds new scanned linked organization to the state', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + const newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + expect(newState.byLaoId[serializedMockLaoId2].allScannedLaos).toEqual([ + mockOrganizationState, + ]); + }); + + it('throws an error if the store already contains an linked organization with the same id', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + const newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + expect(newState.byLaoId[serializedMockLaoId2].allLaoIds).toEqual([serializedMockLaoId]); + expect(() => + linkedOrganizationsReduce( + newState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ), + ).toThrow(); + }); + }); + + describe('removeScannedLinkedOrganization', () => { + it('removes new scanned linked organization from the state', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + let newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + newState = linkedOrganizationsReduce( + newState, + removeScannedLinkedOrganization(mockLaoId2, mockOrganizationState.lao_id), + ); + expect(newState.byLaoId[serializedMockLaoId2].allScannedLaos).toEqual([]); + }); + }); }); describe('makeSingleLinkedOrganizationsSelector', () => { @@ -163,10 +215,10 @@ describe('makeLinkedOrganizationsSelector', () => { }, } as LinkedOrganizationReducerState, }), - ).toEqual([mockOrganizationState]); + ).toEqual([serializedMockLaoId]); }); - it('returns undefined if the linked organization is not in the store', () => { + it('returns empty array if the linked organization is not in the store', () => { const serializedMockLaoId2 = mockLaoId2.valueOf(); const newState = linkedOrganizationsReduce( { @@ -192,4 +244,60 @@ describe('makeLinkedOrganizationsSelector', () => { }), ).toEqual([]); }); + + describe('makeScannedLinkedOrganizationSelector', () => { + it('returns the correct scanned linked organization', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + const newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + expect(newState.byLaoId[serializedMockLaoId2].allScannedLaos).toEqual([ + mockOrganizationState, + ]); + expect( + makeScannedLinkedOrganizationSelector(mockLaoId2)({ + [LINKEDORGANIZATIONS_REDUCER_PATH]: { + byLaoId: { + [serializedMockLaoId2]: { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [mockOrganizationState], + }, + }, + } as LinkedOrganizationReducerState, + }), + ).toEqual([mockOrganizationState]); + }); + + it('returns empty array if the scanned linked organization is not in the store', () => { + const serializedMockLaoId2 = mockLaoId2.valueOf(); + const newState = linkedOrganizationsReduce( + { + byLaoId: {}, + } as LinkedOrganizationReducerState, + addScannedLinkedOrganization(mockLaoId2, mockOrganizationState), + ); + expect(newState.byLaoId[serializedMockLaoId2].allScannedLaos).toEqual([ + mockOrganizationState, + ]); + expect( + makeScannedLinkedOrganizationSelector(mockLaoId)({ + [LINKEDORGANIZATIONS_REDUCER_PATH]: { + byLaoId: { + [serializedMockLaoId2]: { + allLaoIds: [], + byLinkedLaoId: {}, + allLaos: [], + allScannedLaos: [mockOrganizationState], + }, + }, + } as LinkedOrganizationReducerState, + }), + ).toEqual([]); + }); + }); }); diff --git a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx index c1630c646e..f7eecaafaf 100644 --- a/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx +++ b/fe1-web/src/features/linked-organizations/screens/LinkedOrganizationsScreen.tsx @@ -1,15 +1,28 @@ import { ListItem } from '@rneui/themed'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Text, View, StyleSheet, ViewStyle } from 'react-native'; +import { useToast } from 'react-native-toast-notifications'; import { useSelector } from 'react-redux'; import { PoPIcon } from 'core/components'; import ScreenWrapper from 'core/components/ScreenWrapper'; +import { catchup, subscribeToChannel } from 'core/network'; +import { channelFromIds, Hash } from 'core/objects'; +import { dispatch } from 'core/redux'; import { List, Typography } from 'core/styles'; +import { FOUR_SECONDS } from 'resources/const'; import STRINGS from 'resources/strings'; +import BroadcastLinkedOrgInfo from '../components/BroadcastLinkedOrgInfo'; import { LinkedOrganizationsHooks } from '../hooks'; -import { makeLinkedOrganizationSelector } from '../reducer/LinkedOrganizationsReducer'; +import { LinkedOrganization } from '../objects/LinkedOrganization'; +import { makeChallengeReceveidSelector, removeReceivedChallenge } from '../reducer'; +import { + addLinkedOrganization, + makeLinkedOrganizationSelector, + makeScannedLinkedOrganizationSelector, + removeScannedLinkedOrganization, +} from '../reducer/LinkedOrganizationsReducer'; const styles = StyleSheet.create({ flexibleView: { @@ -19,9 +32,68 @@ const styles = StyleSheet.create({ const LinkedOrganizationsScreen = () => { const laoId = LinkedOrganizationsHooks.useCurrentLaoId(); + const toast = useToast(); const isOrganizer = LinkedOrganizationsHooks.useIsLaoOrganizer(laoId); const linkedOrganizationSelector = useMemo(() => makeLinkedOrganizationSelector(laoId), [laoId]); - const linkedOrganizationStates = useSelector(linkedOrganizationSelector); + const linkedOrganizationIds = useSelector(linkedOrganizationSelector); + + const recvChallengeSelector = useMemo(() => makeChallengeReceveidSelector(laoId), [laoId]); + const recvChallengeState = useSelector(recvChallengeSelector); + + const scannedLinkedOrgSelector = useMemo( + () => makeScannedLinkedOrganizationSelector(laoId), + [laoId], + ); + const scannedLinkedOrgStates = useSelector(scannedLinkedOrgSelector); + const [linkedLaoId, setLinkedLaoId] = useState(null); + + useEffect(() => { + const fetchData = async (linkedOrgId: Hash) => { + const channel = channelFromIds(linkedOrgId); + await subscribeToChannel(linkedOrgId, dispatch, channel); + await catchup(channel); + // sometimes there are erros without the extra waiting time - temporary fix + await new Promise((f) => setTimeout(f, 1000)); + setLinkedLaoId(linkedOrgId); + }; + if ( + recvChallengeState && + scannedLinkedOrgStates && + recvChallengeState.length !== 0 && + scannedLinkedOrgStates.length !== 0 && + isOrganizer + ) { + try { + for (const [challenge, publicKey] of recvChallengeState) { + const matchingOrg = scannedLinkedOrgStates.find( + (org) => + org.challenge!.value.valueOf() === challenge.value.valueOf() && + org.challenge!.valid_until.valueOf() === challenge.valid_until.valueOf(), + ); + if (matchingOrg && publicKey) { + dispatch(addLinkedOrganization(laoId, matchingOrg!)); + toast.show(`LAO linked successfully`, { + type: 'success', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + dispatch(removeScannedLinkedOrganization(laoId, matchingOrg.lao_id)); + dispatch(removeReceivedChallenge(laoId, challenge, publicKey)); + const linkedOrg = LinkedOrganization.fromState(matchingOrg); + fetchData(linkedOrg.lao_id); + } else { + toast.show(`Could not link organizations`, { + type: 'danger', + placement: 'bottom', + duration: FOUR_SECONDS, + }); + } + } + } catch (e) { + console.log(e); + } + } + }, [recvChallengeState, laoId, linkedLaoId, toast, scannedLinkedOrgStates, isOrganizer]); return ( @@ -32,17 +104,18 @@ const LinkedOrganizationsScreen = () => { : STRINGS.linked_organizations_description} - {linkedOrganizationStates.map((linkedOrgState) => ( - + {linkedOrganizationIds.map((id) => ( + - {STRINGS.linked_organizations_LaoID} {linkedOrgState.lao_id.valueOf()} + {STRINGS.linked_organizations_LaoID} {id} ))} + {linkedLaoId && } ); diff --git a/fe1-web/src/features/linked-organizations/screens/__tests__/LinkedOrganizationsScreen.test.tsx b/fe1-web/src/features/linked-organizations/screens/__tests__/LinkedOrganizationsScreen.test.tsx index ba3cbb8f9e..12fa7f4103 100644 --- a/fe1-web/src/features/linked-organizations/screens/__tests__/LinkedOrganizationsScreen.test.tsx +++ b/fe1-web/src/features/linked-organizations/screens/__tests__/LinkedOrganizationsScreen.test.tsx @@ -21,6 +21,8 @@ const mockLinkedOrganizationsContextValue = (isOrganizer: boolean) => ({ useConnectedToLao: () => true, useIsLaoOrganizer: () => isOrganizer, useCurrentLao: () => mockLao, + getLaoById: () => undefined, + getRollCallById: () => undefined, } as LinkedOrganizationsReactContext, }); diff --git a/fe1-web/src/features/linked-organizations/screens/__tests__/__snapshots__/LinkedOrganizationsScreen.test.tsx.snap b/fe1-web/src/features/linked-organizations/screens/__tests__/__snapshots__/LinkedOrganizationsScreen.test.tsx.snap index f7c541482a..4891f5b0be 100644 --- a/fe1-web/src/features/linked-organizations/screens/__tests__/__snapshots__/LinkedOrganizationsScreen.test.tsx.snap +++ b/fe1-web/src/features/linked-organizations/screens/__tests__/__snapshots__/LinkedOrganizationsScreen.test.tsx.snap @@ -804,7 +804,7 @@ exports[`LinkedOrganizationsScreen manual input of correct organization details } } > - Join Organization + Invite an Organization - Link Organization + Join an Invitation - Join Organization + Invite an Organization - Link Organization + Join an Invitation - Link Organization + Join an Invitation ([ [k(FEDERATION, CHALLENGE_REQUEST), { type: WitnessingType.NO_WITNESSING }], [k(FEDERATION, FEDERATION_INIT), { type: WitnessingType.NO_WITNESSING }], [k(FEDERATION, FEDERATION_EXPECT), { type: WitnessingType.NO_WITNESSING }], + [k(FEDERATION, FEDERATION_RESULT), { type: WitnessingType.NO_WITNESSING }], + [k(FEDERATION, TOKENS_EXCHANGE), { type: WitnessingType.NO_WITNESSING }], ]); const getWitnessRegistryEntry = (data: MessageData): WitnessEntry | undefined => { diff --git a/fe1-web/src/resources/strings.ts b/fe1-web/src/resources/strings.ts index 30134b2d84..f498b923c3 100644 --- a/fe1-web/src/resources/strings.ts +++ b/fe1-web/src/resources/strings.ts @@ -549,9 +549,9 @@ namespace STRINGS { export const linked_organizations_description = 'Here you can find all linked organizations.'; export const linked_organizations_addlinkedorg_title = 'Add Linked Organization'; export const linked_organizations_addlinkedorg_info = - 'To link two organizations, one organizer has to first join the other organization by generating a QR Code (Join Organization), which other one links to his organization by scanning the QR Code (Link Organization). Subsequent to scanning the first QR Code, the other QR Code will appear. After the first QR Code was scanned, the other scanner for the second QR Code can be opened by clicking the “Next” button.'; - export const linked_organizations_addlinkedorg_genQRCode = 'Join Organization'; - export const linked_organizations_addlinkedorg_scanQRCode = 'Link Organization'; + 'To link two organizations, one organizer has to first invite the other organization by generating a QR Code (Invite an Organization). The other organizer joins this invitation, by scanning the QR Code (Join an Invitation). Subsequent to scanning the first QR Code, the other QR Code will appear. After the first QR Code was scanned, the other scanner for the second QR Code can be opened by clicking the “Next” button.'; + export const linked_organizations_addlinkedorg_genQRCode = 'Invite an Organization'; + export const linked_organizations_addlinkedorg_scanQRCode = 'Join an Invitation'; export const linked_organizations_addlinkedorg_Scanner_info = 'Scan the QR Code from the other organizers device.'; export const linked_organizations_addlinkedorg_QRCode_info =