From 452dbe8bba39e35bd0e32b5d1349d9ae962343ff Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Mon, 10 Feb 2025 16:34:38 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(front)=20add=20sdk=20related=20routes?= =?UTF-8?q?=20and=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The create room button is a dedicated route. There is also a bit of logic implied in this commit, including the BroadcastChannel. The router has been updated with a /sdk negation in order to avoid including support and react query debug tool in the iframe. --- src/frontend/src/App.tsx | 37 +++++-- src/frontend/src/assets/VisioIcon.tsx | 24 ++++ .../src/features/home/routes/Home.tsx | 15 ++- .../src/features/sdk/SdkReverseClient.tsx | 84 ++++++++++++++ .../src/features/sdk/routes/CreateButton.tsx | 103 ++++++++++++++++++ src/frontend/src/locales/de/sdk.json | 5 + src/frontend/src/locales/en/sdk.json | 5 + src/frontend/src/locales/fr/sdk.json | 5 + 8 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 src/frontend/src/assets/VisioIcon.tsx create mode 100644 src/frontend/src/features/sdk/SdkReverseClient.tsx create mode 100644 src/frontend/src/features/sdk/routes/CreateButton.tsx create mode 100644 src/frontend/src/locales/de/sdk.json create mode 100644 src/frontend/src/locales/en/sdk.json create mode 100644 src/frontend/src/locales/fr/sdk.json diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index cbe694ef..792f3bd8 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -13,24 +13,41 @@ import { routes } from './routes' import './i18n/init' import { queryClient } from '@/api/queryClient' import { AppInitialization } from '@/components/AppInitialization' +import { SdkCreateButton } from './features/sdk/routes/CreateButton' function App() { const { i18n } = useTranslation() useLang(i18n.language) return ( - - - - {Object.entries(routes).map(([, route], i) => ( - - ))} - - - - + + + + + + + {/* We only want support and ReactQueryDevTools in non /sdk routes */} + + + + {Object.entries(routes).map(([, route], i) => ( + + ))} + + + + + + diff --git a/src/frontend/src/assets/VisioIcon.tsx b/src/frontend/src/assets/VisioIcon.tsx new file mode 100644 index 00000000..ba4cc801 --- /dev/null +++ b/src/frontend/src/assets/VisioIcon.tsx @@ -0,0 +1,24 @@ +export const VisioIcon = () => { + return ( + + + + + + ) +} diff --git a/src/frontend/src/features/home/routes/Home.tsx b/src/frontend/src/features/home/routes/Home.tsx index 113affc8..1b89cec8 100644 --- a/src/frontend/src/features/home/routes/Home.tsx +++ b/src/frontend/src/features/home/routes/Home.tsx @@ -13,11 +13,12 @@ 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, useState } from 'react' +import { ReactNode, useEffect, 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,6 +157,18 @@ 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/sdk/SdkReverseClient.tsx b/src/frontend/src/features/sdk/SdkReverseClient.tsx new file mode 100644 index 00000000..ca4c5873 --- /dev/null +++ b/src/frontend/src/features/sdk/SdkReverseClient.tsx @@ -0,0 +1,84 @@ +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, ...other } = useUser({ + fetchUserOptions: { attemptSilent: false }, + }) + + const startSSO = () => { + return new Promise((resolve) => { + SdkReverseClient.waitForAuthenticationAck().then(async () => { + await other.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/routes/CreateButton.tsx b/src/frontend/src/features/sdk/routes/CreateButton.tsx new file mode 100644 index 00000000..8d1ae610 --- /dev/null +++ b/src/frontend/src/features/sdk/routes/CreateButton.tsx @@ -0,0 +1,103 @@ +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/locales/de/sdk.json b/src/frontend/src/locales/de/sdk.json new file mode 100644 index 00000000..4e02be40 --- /dev/null +++ b/src/frontend/src/locales/de/sdk.json @@ -0,0 +1,5 @@ +{ + "createButton": { + "label": "" + } +} diff --git a/src/frontend/src/locales/en/sdk.json b/src/frontend/src/locales/en/sdk.json new file mode 100644 index 00000000..5d713724 --- /dev/null +++ b/src/frontend/src/locales/en/sdk.json @@ -0,0 +1,5 @@ +{ + "createButton": { + "label": "Create a Visio link" + } +} diff --git a/src/frontend/src/locales/fr/sdk.json b/src/frontend/src/locales/fr/sdk.json new file mode 100644 index 00000000..0bf7a141 --- /dev/null +++ b/src/frontend/src/locales/fr/sdk.json @@ -0,0 +1,5 @@ +{ + "createButton": { + "label": "Créer un lien Visio" + } +}