+ {roomUrl && room?.slug ? (
+
+
+
+
+ {t('joinButton')}
+
+
+ }
+ tooltip={t('copyLinkTooltip')}
+ onPress={() => {
+ navigator.clipboard.writeText(roomUrl)
+ }}
+ />
+ {searchParams.get('readOnly') === 'false' && (
+ }
+ onPress={resetState}
+ aria-label={t('resetLabel')}
+ />
+ )}
+
+
+
+
+ {roomUrl.replace('https://', '')}
+
+
+ {t('participantLimit')}
+
+
+
+ ) : (
+
+ {/*
+ * Using popup for Visio to access session cookies (blocked in iframes).
+ * If authenticated: Popup creates room and returns data directly.
+ * If not: Popup sends callbackId, redirects to login, then backend
+ * associates new room with callbackId after authentication.
+ */}
+ {
+ setIsPending(true)
+ popupManager.createPopupWindow(() => {
+ setIsPending(false)
+ })
+ }}
+ size="sm"
+ >
+
+ {t('createButton')}
+
+
+ )}
+
+ )
+}
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 (
+ ) => 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() {
Visioconference
-
+ setRoomUrl(data.url)} />
Description
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