diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 2ccde918..72607b96 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -5,7 +5,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { QueryClientProvider } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { useLang } from 'hoofd' -import { Switch, Route, useLocation } from 'wouter' +import { Switch, Route } from 'wouter' import { I18nProvider } from 'react-aria-components' import { Layout } from './layout/Layout' import { NotFoundScreen } from './components/NotFoundScreen' @@ -13,35 +13,11 @@ import { routes } from './routes' import './i18n/init' import { queryClient } from '@/api/queryClient' import { AppInitialization } from '@/components/AppInitialization' -import { SdkCreateButton } from './features/sdk/routes/CreateButton' - -const SDK_BASE_ROUTE = '/sdk' function App() { const { i18n } = useTranslation() useLang(i18n.language) - const [location] = useLocation() - const isSDKRoute = location.startsWith(SDK_BASE_ROUTE) - - if (isSDKRoute) { - return ( - - - - - - - - - - - - - - ) - } - return ( diff --git a/src/frontend/src/api/queryKeys.ts b/src/frontend/src/api/queryKeys.ts index 96c01d77..ccb21d29 100644 --- a/src/frontend/src/api/queryKeys.ts +++ b/src/frontend/src/api/queryKeys.ts @@ -4,4 +4,5 @@ export const keys = { config: 'config', requestEntry: 'requestEntry', waitingParticipants: 'waitingParticipants', + roomCreationCallback: 'roomCreationCallback', } diff --git a/src/frontend/src/components/AppInitialization.tsx b/src/frontend/src/components/AppInitialization.tsx index 1882a292..f4d37f69 100644 --- a/src/frontend/src/components/AppInitialization.tsx +++ b/src/frontend/src/components/AppInitialization.tsx @@ -2,9 +2,13 @@ import { silenceLiveKitLogs } from '@/utils/livekit' import { useConfig } from '@/api/useConfig' import { useAnalytics } from '@/features/analytics/hooks/useAnalytics' import { useSupport } from '@/features/support/hooks/useSupport' +import { useLocation } from 'wouter' + +const SDK_BASE_ROUTE = '/sdk' export const AppInitialization = () => { const { data } = useConfig() + const [location] = useLocation() const { analytics = {}, @@ -12,8 +16,11 @@ export const AppInitialization = () => { silence_livekit_debug_logs = false, } = data || {} - useAnalytics(analytics) - useSupport(support) + const isSDKContext = location.includes(SDK_BASE_ROUTE) + + useAnalytics({ ...analytics, isDisabled: isSDKContext }) + useSupport({ ...support, isDisabled: isSDKContext }) + silenceLiveKitLogs(silence_livekit_debug_logs) return null diff --git a/src/frontend/src/features/analytics/hooks/useAnalytics.ts b/src/frontend/src/features/analytics/hooks/useAnalytics.ts index ed0f87a4..7149c54d 100644 --- a/src/frontend/src/features/analytics/hooks/useAnalytics.ts +++ b/src/frontend/src/features/analytics/hooks/useAnalytics.ts @@ -17,18 +17,19 @@ export const terminateAnalyticsSession = () => { export type useAnalyticsProps = { id?: string host?: string + isDisabled?: boolean } -export const useAnalytics = ({ id, host }: useAnalyticsProps) => { +export const useAnalytics = ({ id, host, isDisabled }: useAnalyticsProps) => { const [location] = useLocation() useEffect(() => { - if (!id || !host) return + if (!id || !host || isDisabled) return if (posthog.__loaded) return posthog.init(id, { api_host: host, person_profiles: 'always', }) - }, [id, host]) + }, [id, host, isDisabled]) // From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context. useEffect(() => { diff --git a/src/frontend/src/features/home/routes/Home.tsx b/src/frontend/src/features/home/routes/Home.tsx index 7d697238..7ae473ab 100644 --- a/src/frontend/src/features/home/routes/Home.tsx +++ b/src/frontend/src/features/home/routes/Home.tsx @@ -13,12 +13,11 @@ import { RiAddLine, RiLink } from '@remixicon/react' import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog' import { IntroSlider } from '@/features/home/components/IntroSlider' import { MoreLink } from '@/features/home/components/MoreLink' -import { ReactNode, useEffect, useState } from 'react' +import { ReactNode, useState } from 'react' import { css } from '@/styled-system/css' import { menuRecipe } from '@/primitives/menuRecipe.ts' import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices' -import { SdkReverseClient } from '@/features/sdk/SdkReverseClient' const Columns = ({ children }: { children?: ReactNode }) => { return ( @@ -156,18 +155,6 @@ export const Home = () => { const { mutateAsync: createRoom } = useCreateRoom() const [laterRoomId, setLaterRoomId] = useState(null) - const { user } = useUser() - - /** - * Used for SDK popup to close automatically. - */ - useEffect(() => { - if (!user) { - return - } - SdkReverseClient.broadcastAuthentication() - }, [user]) - return ( diff --git a/src/frontend/src/features/rooms/api/createRoom.ts b/src/frontend/src/features/rooms/api/createRoom.ts index 2ffef040..c08a1b5f 100644 --- a/src/frontend/src/features/rooms/api/createRoom.ts +++ b/src/frontend/src/features/rooms/api/createRoom.ts @@ -5,17 +5,20 @@ import { ApiRoom } from './ApiRoom' export interface CreateRoomParams { slug: string + callbackId?: string username?: string } const createRoom = ({ slug, + callbackId, username = '', }: CreateRoomParams): Promise => { return fetchApi(`rooms/?username=${encodeURIComponent(username)}`, { method: 'POST', body: JSON.stringify({ name: slug, + callback_id: callbackId, }), }) } diff --git a/src/frontend/src/features/sdk/SdkReverseClient.tsx b/src/frontend/src/features/sdk/SdkReverseClient.tsx deleted file mode 100644 index ffb7ab7f..00000000 --- a/src/frontend/src/features/sdk/SdkReverseClient.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { authUrl, useUser } from '../auth' - -export enum ClientMessageType { - ROOM_CREATED = 'ROOM_CREATED', -} - -export class SdkReverseClient { - /** - * IDEA: Use API Key. Must be based on some sort of credentials? No needs for now as there are no security - * plausible at the moment. - */ - static getAllowTargetOrigin() { - return '*' - } - - static post(type: ClientMessageType, data: unknown = {}) { - window.parent.postMessage( - { - type, - data, - }, - SdkReverseClient.getAllowTargetOrigin() - ) - } - - static broadcastAuthentication() { - const bc = new BroadcastChannel('APP_CHANNEL') - bc.postMessage({ type: 'AUTHENTICATED' }) - - /** - * This means the parent window has authenticated has successfully refetched user, then we can close the popup. - */ - bc.onmessage = (event) => { - if (event.data.type === 'AUTHENTICATED_ACK') { - window.close() - } - } - } - - static waitForAuthenticationAck() { - return new Promise((resolve) => { - const bc = new BroadcastChannel('APP_CHANNEL') - bc.onmessage = async (event) => { - if (event.data.type === 'AUTHENTICATED') { - resolve() - bc.postMessage({ type: 'AUTHENTICATED_ACK' }) - } - } - }) - } -} - -/** - * Returns a function to be awaited in order to make sure the user is logged in. - * If not logged-in it opens a popup with the connection flow, the promise returned is resolved - * once logged-in. - * - * To be used in SDK scope. - */ -export function useEnsureAuth() { - const { isLoggedIn, refetch } = useUser({ - fetchUserOptions: { attemptSilent: false }, - }) - - const startSSO = () => { - return new Promise((resolve) => { - SdkReverseClient.waitForAuthenticationAck().then(async () => { - await refetch() - resolve() - }) - const params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no, - width=400,height=900,left=100,top=100` - window.open(new URL('authenticate/', authUrl()).href, '', params) - }) - } - - const ensureAuth = async () => { - if (!isLoggedIn) { - await startSSO() - } - } - - return { ensureAuth } -} diff --git a/src/frontend/src/features/sdk/api/useRoomCreationCallback.ts b/src/frontend/src/features/sdk/api/useRoomCreationCallback.ts new file mode 100644 index 00000000..f56bb6ee --- /dev/null +++ b/src/frontend/src/features/sdk/api/useRoomCreationCallback.ts @@ -0,0 +1,35 @@ +import { fetchApi } from '@/api/fetchApi' +import { useQuery } from '@tanstack/react-query' +import { keys } from '@/api/queryKeys' +import { CallbackCreationRoomData } from '../utils/types' + +export type CallbackResponse = { + status: string + room: CallbackCreationRoomData +} + +export const fetchRoomGenerationState = async ({ + callbackId, +}: { + callbackId: string +}) => { + return fetchApi(`/rooms/creation-callback/`, { + method: 'POST', + body: JSON.stringify({ + callback_id: callbackId, + }), + }) +} + +export const useRoomCreationCallback = ({ + callbackId = '', +}: { + callbackId?: string +}) => { + return useQuery({ + queryKey: [keys.roomCreationCallback, callbackId], + queryFn: () => fetchRoomGenerationState({ callbackId }), + enabled: !!callbackId, + refetchInterval: 1000, + }) +} diff --git a/src/frontend/src/features/sdk/routes/CreateButton.tsx b/src/frontend/src/features/sdk/routes/CreateButton.tsx deleted file mode 100644 index 8d1ae610..00000000 --- a/src/frontend/src/features/sdk/routes/CreateButton.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Button } from '@/primitives/Button' -import { useTranslation } from 'react-i18next' -import { usePersistentUserChoices } from '@livekit/components-react' -import { useState } from 'react' -import { getRouteUrl } from '@/navigation/getRouteUrl' -import { css } from '@/styled-system/css' -import { RiCheckLine, RiFileCopyLine } from '@remixicon/react' -import { VisioIcon } from '@/assets/VisioIcon' -import { generateRoomId, useCreateRoom } from '../../rooms' -import { - ClientMessageType, - SdkReverseClient, - useEnsureAuth, -} from '../SdkReverseClient' - -export const SdkCreateButton = () => { - const { t } = useTranslation('sdk', { keyPrefix: 'createButton' }) - const [roomUrl, setRoomUrl] = useState() - const [isLoading, setIsLoading] = useState(false) - const { - userChoices: { username }, - } = usePersistentUserChoices() - - const { mutateAsync: createRoom } = useCreateRoom() - const { ensureAuth } = useEnsureAuth() - - const submitCreateRoom = async () => { - setIsLoading(true) - const slug = generateRoomId() - const data = await createRoom({ slug, username }) - const roomUrlTmp = getRouteUrl('room', data.slug) - setRoomUrl(roomUrlTmp) - setIsLoading(false) - SdkReverseClient.post(ClientMessageType.ROOM_CREATED, { - url: roomUrlTmp, - }) - } - - const submit = async () => { - await ensureAuth() - submitCreateRoom() - } - - return ( -
- {roomUrl ? ( - - ) : ( - - )} -
- ) -} - -const RoomUrl = ({ roomUrl }: { roomUrl: string }) => { - const [isCopied, setIsCopied] = useState(false) - - const copy = () => { - navigator.clipboard.writeText(roomUrl!) - setIsCopied(true) - setTimeout(() => setIsCopied(false), 1000) - } - - return ( -
- - {roomUrl} - - -
- ) -} diff --git a/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx b/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx new file mode 100644 index 00000000..dd0936bf --- /dev/null +++ b/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx @@ -0,0 +1,164 @@ +import { Button } from '@/primitives/Button' +import { useEffect, useMemo, useState } from 'react' +import { Link } from 'react-aria-components' +import { useTranslation } from 'react-i18next' +import { HStack, VStack } from '@/styled-system/jsx' +import { css } from '@/styled-system/css' +import { RiCloseLine, RiFileCopyLine } from '@remixicon/react' +import { Text } from '@/primitives' +import { Spinner } from '@/primitives/Spinner' +import { buttonRecipe } from '@/primitives/buttonRecipe' +import { VisioIcon } from '@/assets/VisioIcon' +import { getRouteUrl } from '@/navigation/getRouteUrl' +import { useRoomCreationCallback } from '../api/useRoomCreationCallback' +import { PopupManager } from '../utils/PopupManager' +import { CallbackCreationRoomData } from '../utils/types' +import { useSearchParams } from 'wouter' + +const popupManager = new PopupManager() + +export const CreateMeetingButton = () => { + const { t } = useTranslation('sdk', { keyPrefix: 'createMeeting' }) + + const [searchParams] = useSearchParams() + + const [callbackId, setCallbackId] = useState(undefined) + const [isPending, setIsPending] = useState(false) + + const initialRoom = useMemo(() => { + const roomSlug = searchParams.get('slug') + if (!roomSlug) return undefined + return { + slug: roomSlug.trim(), // Trim whitespace for safety + } + }, [searchParams]) + + const [room, setRoom] = useState( + initialRoom + ) + + const { data } = useRoomCreationCallback({ callbackId }) + + const roomUrl = useMemo(() => { + if (room?.slug) return getRouteUrl('room', room.slug) + }, [room]) + + useEffect(() => { + if (!data?.room?.slug) return + setRoom(data.room) + setCallbackId(undefined) + setIsPending(false) + }, [data]) + + useEffect(() => { + popupManager.setupMessageListener( + (id) => setCallbackId(id), + (data) => { + setRoom(data) + setIsPending(false) + } + ) + + return () => popupManager.cleanup() + }, []) + + const resetState = () => { + setRoom(undefined) + setCallbackId(undefined) + setIsPending(false) + } + + if (isPending) { + return ( +
+ +
+ ) + } + + return ( +
+ {roomUrl && room?.slug ? ( + + + + + {t('joinButton')} + + + +
+ )} + + ) +} diff --git a/src/frontend/src/features/sdk/routes/CreatePopup.tsx b/src/frontend/src/features/sdk/routes/CreatePopup.tsx new file mode 100644 index 00000000..6cd5b3cd --- /dev/null +++ b/src/frontend/src/features/sdk/routes/CreatePopup.tsx @@ -0,0 +1,76 @@ +import { useEffect, useMemo } from 'react' +import { css } from '@/styled-system/css' +import { generateRoomId, useCreateRoom } from '../../rooms' +import { useUser } from '@/features/auth' +import { Spinner } from '@/primitives/Spinner' +import { CallbackIdHandler } from '../utils/CallbackIdHandler' +import { PopupWindow } from '../utils/PopupWindow' + +const callbackIdHandler = new CallbackIdHandler() +const popupWindow = new PopupWindow() + +export const CreatePopup = () => { + const { isLoggedIn } = useUser({ fetchUserOptions: { attemptSilent: false } }) + const { mutateAsync: createRoom } = useCreateRoom() + + const callbackId = useMemo(() => callbackIdHandler.getOrCreate(), []) + + /** + * Handle unauthenticated users by redirecting to login + * + * When redirecting to authentication, the window.location change breaks the connection + * between this popup and its parent window. We need to send the callbackId to the parent + * before redirecting so it can re-establish connection after authentication completes. + * This prevents the popup from becoming orphaned and ensures state consistency. + */ + useEffect(() => { + if (isLoggedIn === false) { + // redirection loses the connection to the manager + // prevent it passing an async callback id + popupWindow.sendCallbackId(callbackId, () => { + popupWindow.navigateToAuthentication() + }) + } + }, [isLoggedIn, callbackId]) + + /** + * Automatically create meeting room once user is authenticated + * This effect will trigger either immediately if the user is already logged in, + * or after successful authentication and return to this popup + */ + useEffect(() => { + const createMeetingRoom = async () => { + try { + const slug = generateRoomId() + const roomData = await createRoom({ + slug, + callbackId, + }) + // Send room data back to parent window and clean up resources + popupWindow.sendRoomData(roomData, () => { + callbackIdHandler.clear() + popupWindow.close() + }) + } catch (error) { + console.error('Failed to create meeting room:', error) + } + } + if (isLoggedIn && callbackId) { + createMeetingRoom() + } + }, [isLoggedIn, callbackId, createRoom]) + + return ( +
+ +
+ ) +} diff --git a/src/frontend/src/features/sdk/utils/CallbackIdHandler.ts b/src/frontend/src/features/sdk/utils/CallbackIdHandler.ts new file mode 100644 index 00000000..1e071eb9 --- /dev/null +++ b/src/frontend/src/features/sdk/utils/CallbackIdHandler.ts @@ -0,0 +1,45 @@ +export class CallbackIdHandler { + private storageKey = 'popup_callback_id' + + private generateId(): string { + return ( + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + ) + } + + /** + * Gets an existing callback ID or creates a new one + */ + public getOrCreate(): string { + const existingId = this.get() + if (existingId) { + return existingId + } + + const newId = this.generateId() + this.set(newId) + return newId + } + + /** + * Gets the current callback ID if one exists + */ + public get(): string | null { + return sessionStorage.getItem(this.storageKey) + } + + /** + * Sets a callback ID + */ + private set(id: string): void { + sessionStorage.setItem(this.storageKey, id) + } + + /** + * Removes the current callback ID + */ + public clear(): void { + sessionStorage.removeItem(this.storageKey) + } +} diff --git a/src/frontend/src/features/sdk/utils/PopupManager.ts b/src/frontend/src/features/sdk/utils/PopupManager.ts new file mode 100644 index 00000000..d5f9889f --- /dev/null +++ b/src/frontend/src/features/sdk/utils/PopupManager.ts @@ -0,0 +1,63 @@ +import { getRouteUrl } from '@/navigation/getRouteUrl' +import { + CallbackCreationRoomData, + ClientMessageType, + PopupMessageData, + PopupMessageType, +} from './types' + +export class PopupManager { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private messageHandler: (event: MessageEvent) => void = () => {} + + public createPopupWindow(onFailure: () => void) { + const popupWindow = window.open( + `${window.location.origin}/sdk/create-popup`, + 'CreatePopupWindow', + `status=no,location=no,toolbar=no,menubar=no,width=600,height=800,left=100,top=100, resizable=yes,scrollbars=yes` + ) + + if (popupWindow) { + popupWindow.focus() + } else { + onFailure() + } + } + + public setupMessageListener( + onCallbackId: (id: string) => void, + onRoomData: (data: CallbackCreationRoomData) => void + ) { + this.messageHandler = (event) => { + const data = event.data as PopupMessageData + // Skip messages from untrusted sources + if (data.source !== window.location.origin) return + switch (data.type) { + case PopupMessageType.CALLBACK_ID: + onCallbackId(data.callbackId as string) + return + case PopupMessageType.ROOM_DATA: + if (!data?.room) return + onRoomData(data.room) + window?.parent.postMessage( + { + type: ClientMessageType.ROOM_CREATED, + data: { + room: { + url: getRouteUrl('room', data.room.slug), + ...data.room, + }, + }, + }, + '*' + ) + return + } + } + window.addEventListener('message', this.messageHandler) + } + + public cleanup() { + window.removeEventListener('message', this.messageHandler) + } +} diff --git a/src/frontend/src/features/sdk/utils/PopupWindow.ts b/src/frontend/src/features/sdk/utils/PopupWindow.ts new file mode 100644 index 00000000..15856c4b --- /dev/null +++ b/src/frontend/src/features/sdk/utils/PopupWindow.ts @@ -0,0 +1,50 @@ +import { authUrl } from '@/features/auth' +import { PopupMessageType, CallbackCreationRoomData } from './types' + +export class PopupWindow { + private sendMessageToManager( + type: PopupMessageType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any, + callback?: () => void + ) { + if (!window.opener) { + console.error('No manager window found') + window.close() + return + } + window.opener.postMessage( + { + source: window.location.origin, + type, + ...data, + }, + window.location.origin + ) + callback?.() + } + + public sendRoomData(data: CallbackCreationRoomData, callback?: () => void) { + this.sendMessageToManager( + PopupMessageType.ROOM_DATA, + { room: { slug: data.slug } }, + callback + ) + } + + public sendCallbackId(callbackId: string, callback?: () => void) { + this.sendMessageToManager( + PopupMessageType.CALLBACK_ID, + { callbackId }, + callback + ) + } + + public close() { + window.close() + } + + public navigateToAuthentication() { + window.location.href = authUrl({}) + } +} diff --git a/src/frontend/src/features/sdk/utils/types.ts b/src/frontend/src/features/sdk/utils/types.ts new file mode 100644 index 00000000..1c9c4bde --- /dev/null +++ b/src/frontend/src/features/sdk/utils/types.ts @@ -0,0 +1,19 @@ +export type CallbackCreationRoomData = { + slug: string +} + +export enum ClientMessageType { + ROOM_CREATED = 'ROOM_CREATED', +} + +export interface PopupMessageData { + type: PopupMessageType + source: string + callbackId?: string + room?: CallbackCreationRoomData +} + +export enum PopupMessageType { + CALLBACK_ID, + ROOM_DATA, +} diff --git a/src/frontend/src/features/support/hooks/useSupport.tsx b/src/frontend/src/features/support/hooks/useSupport.tsx index 4888fb90..c7c84278 100644 --- a/src/frontend/src/features/support/hooks/useSupport.tsx +++ b/src/frontend/src/features/support/hooks/useSupport.tsx @@ -18,15 +18,16 @@ export const terminateSupportSession = () => { export type useSupportProps = { id?: string + isDisabled?: boolean } // Configure Crisp chat for real-time support across all pages. -export const useSupport = ({ id }: useSupportProps) => { +export const useSupport = ({ id, isDisabled }: useSupportProps) => { useEffect(() => { - if (!id || Crisp.isCrispInjected()) return + if (!id || Crisp.isCrispInjected() || isDisabled) return Crisp.configure(id) Crisp.setHideOnMobile(true) - }, [id]) + }, [id, isDisabled]) return null } diff --git a/src/frontend/src/locales/de/sdk.json b/src/frontend/src/locales/de/sdk.json index 4e02be40..0452f331 100644 --- a/src/frontend/src/locales/de/sdk.json +++ b/src/frontend/src/locales/de/sdk.json @@ -1,5 +1,10 @@ { - "createButton": { - "label": "" + "createMeeting": { + "createButton": "", + "joinButton": "", + "copyLinkTooltip": "", + "resetLabel": "", + "participantLimit": "", + "popupBlocked": "" } } diff --git a/src/frontend/src/locales/en/sdk.json b/src/frontend/src/locales/en/sdk.json index 5d713724..80438d83 100644 --- a/src/frontend/src/locales/en/sdk.json +++ b/src/frontend/src/locales/en/sdk.json @@ -1,5 +1,10 @@ { - "createButton": { - "label": "Create a Visio link" + "createMeeting": { + "createButton": "Create", + "joinButton": "Join with Visio", + "copyLinkTooltip": "Copy link", + "resetLabel": "Reset", + "participantLimit": "Up to 150 participants.", + "popupBlocked": "Popup was blocked. Please allow popups for this site." } } diff --git a/src/frontend/src/locales/fr/sdk.json b/src/frontend/src/locales/fr/sdk.json index 0bf7a141..9718c43b 100644 --- a/src/frontend/src/locales/fr/sdk.json +++ b/src/frontend/src/locales/fr/sdk.json @@ -1,5 +1,10 @@ { - "createButton": { - "label": "Créer un lien Visio" + "createMeeting": { + "createButton": "Créer un lien", + "joinButton": "Participer avec Visio", + "copyLinkTooltip": "Copier le lien", + "resetLabel": "Réinitialiser", + "participantLimit": "Jusqu'à 150 participants.", + "popupBlocked": "La fenêtre pop-up a été bloquée. Veuillez autoriser les pop-ups pour ce site." } } diff --git a/src/frontend/src/locales/nl/sdk.json b/src/frontend/src/locales/nl/sdk.json index 8da4ee6a..c82ac9a7 100644 --- a/src/frontend/src/locales/nl/sdk.json +++ b/src/frontend/src/locales/nl/sdk.json @@ -1,5 +1,10 @@ { - "createButton": { - "label": "Maak een Visio link" + "createMeeting": { + "createButton": "Aanmaken", + "joinButton": "Deelnemen met Visio", + "copyLinkTooltip": "Link kopiëren", + "resetLabel": "Resetten", + "participantLimit": "Tot 150 deelnemers.", + "popupBlocked": "Pop-up werd geblokkeerd. Sta pop-ups toe voor deze site." } } diff --git a/src/frontend/src/routes.ts b/src/frontend/src/routes.ts index 31780700..0783a15d 100644 --- a/src/frontend/src/routes.ts +++ b/src/frontend/src/routes.ts @@ -3,6 +3,8 @@ import { HomeRoute } from '@/features/home' import { LegalTermsRoute } from '@/features/legalsTerms/LegalTermsRoute' import { AccessibilityRoute } from '@/features/legalsTerms/Accessibility' import { TermsOfServiceRoute } from '@/features/legalsTerms/TermsOfService' +import { CreatePopup } from '@/features/sdk/routes/CreatePopup' +import { CreateMeetingButton } from '@/features/sdk/routes/CreateMeetingButton' export const routes: Record< | 'home' @@ -10,7 +12,9 @@ export const routes: Record< | 'feedback' | 'legalTerms' | 'accessibility' - | 'termsOfService', + | 'termsOfService' + | 'sdkCreatePopup' + | 'sdkCreateButton', { name: RouteName path: RegExp | string @@ -50,6 +54,16 @@ export const routes: Record< path: '/conditions-utilisation', Component: TermsOfServiceRoute, }, + sdkCreatePopup: { + name: 'sdkCreatePopup', + path: '/sdk/create-popup', + Component: CreatePopup, + }, + sdkCreateButton: { + name: 'sdkCreateButton', + path: '/sdk/create-button', + Component: CreateMeetingButton, + }, } export type RouteName = keyof typeof routes diff --git a/src/sdk/consumer/src/App.tsx b/src/sdk/consumer/src/App.tsx index 9535420a..8ecf1f2c 100644 --- a/src/sdk/consumer/src/App.tsx +++ b/src/sdk/consumer/src/App.tsx @@ -21,7 +21,7 @@ function App() {
- + setRoomUrl(data.url)} />
diff --git a/src/sdk/consumer/src/index.css b/src/sdk/consumer/src/index.css index caec1e5f..d3ce013d 100644 --- a/src/sdk/consumer/src/index.css +++ b/src/sdk/consumer/src/index.css @@ -72,3 +72,56 @@ button { font-weight: 300; cursor: pointer; } + +.create-meeting { + background-color: rgb(45, 45, 70); + color: white; + height: 46px; + border-radius: 4px; + font-size: 16px; + font-weight: 300; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 1rem 0.75rem; + border: none; +} + +.create-meeting svg path { + fill: white !important; +} + + +.icon { + color: white !important; +} + +.join-button { + background-color: rgb(45, 45, 70); + color: white; + height: 46px; + border-radius: 4px; + font-size: 16px; + font-weight: 300; + cursor: pointer; + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: center; + width: fit-content; + padding: 0 1rem; + text-decoration: none; +} + +.join-button svg path { + fill: white !important; +} + +.join-link { + padding-top: 0.5rem; +} + +.join-link svg path { + fill: white !important; +} diff --git a/src/sdk/library/.env.development b/src/sdk/library/.env.development new file mode 100644 index 00000000..b0394efa --- /dev/null +++ b/src/sdk/library/.env.development @@ -0,0 +1 @@ +VITE_VISIO_SDK_URL=https://meet.127.0.0.1.nip.io/sdk diff --git a/src/sdk/library/src/Types.ts b/src/sdk/library/src/Types.ts index 5408211a..cf9ee629 100644 --- a/src/sdk/library/src/Types.ts +++ b/src/sdk/library/src/Types.ts @@ -5,3 +5,10 @@ export type ConfigType = typeof DEFAULT_CONFIG export enum ClientMessageType { ROOM_CREATED = 'ROOM_CREATED', } + +export type RoomData = { + slug: string + url: string + phone?: string + code?: string +} diff --git a/src/sdk/library/src/create/VisioCreateButton.tsx b/src/sdk/library/src/create/VisioCreateButton.tsx index a1007a23..f9e89088 100644 --- a/src/sdk/library/src/create/VisioCreateButton.tsx +++ b/src/sdk/library/src/create/VisioCreateButton.tsx @@ -1,11 +1,15 @@ -import { DEFAULT_CONFIG } from '@/Config' -import { ClientMessageType } from '@/Types' import { useEffect } from 'react' +import { ClientMessageType, RoomData } from '@/Types' +import { DEFAULT_CONFIG } from '@/Config' export const VisioCreateButton = ({ onRoomCreated, + readOnly = false, + slug, }: { - onRoomCreated: (roomUrl: string) => void + onRoomCreated: (roomData: RoomData) => void + readOnly?: boolean + slug?: string }) => { useEffect(() => { const onMessage = (event: MessageEvent) => { @@ -13,13 +17,11 @@ export const VisioCreateButton = ({ if (event.origin !== new URL(DEFAULT_CONFIG.url).origin) { return } - if (event.data.type === ClientMessageType.ROOM_CREATED) { - const data = event.data.data - const roomUrl = data.url - onRoomCreated(roomUrl) + const { type, data } = event.data + if (type == ClientMessageType.ROOM_CREATED && data?.room) { + onRoomCreated(data.room) } } - window.addEventListener('message', onMessage) return () => { window.removeEventListener('message', onMessage) @@ -30,10 +32,13 @@ export const VisioCreateButton = ({ // eslint-disable-next-line jsx-a11y/iframe-has-title