Skip to content

Commit 2647b84

Browse files
authored
Add data-sandbox-id attribute to embed script snippet (#45)
* feat: add support for data-lk-sandbox-id attribute Previously, the generated embed popup script didn't work properly because it might be loaded on an origin where the sandbox id couldn't be extracted from the url or be otherwise located. This meant that while this repository could be demo'd effectively, you couldn't really in practice use the script tag popup embed for anything real. This change introduces a new data-lk-sandbox-id attribute which can be specified on the script tag that loads the popup. If set, _this_ sandbox id is used when talking to the sandbox token server endpoint to get a token rather than attempting to extract it from the URL or anything like that. Using this mechanism, a embed popup script can be added to any page and it should always work because the sandbox id is now being included as part of the embed. * feat: make popup page dynamic so sandbox id can be included in the generated script tag * feat: make data-lk-sandbod-id attribute required in all cases going forward
1 parent c524001 commit 2647b84

File tree

6 files changed

+109
-84
lines changed

6 files changed

+109
-84
lines changed

app/(app)/popup/page.tsx

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,5 @@
1-
'use client';
1+
import PopupPageDynamic from '@/components/popup-page-dynamic';
22

3-
import Script from 'next/script';
4-
import { HandPointingIcon } from '@phosphor-icons/react';
5-
import { ThemeToggle } from '@/components/theme-toggle';
6-
7-
export default function PopupPage() {
8-
return (
9-
<div className="grid min-h-screen place-items-center">
10-
<Script src="/embed-popup.js" />
11-
<div className="space-y-10">
12-
<div className="flex justify-center">
13-
<ThemeToggle className="w-fit" />
14-
</div>
15-
<div className="text-fgAccent flex gap-1">
16-
<p className="grow text-sm">
17-
The popup button should appear in the bottom right corner of the screen
18-
</p>
19-
<HandPointingIcon
20-
size={16}
21-
weight="regular"
22-
className="mt-0.5 inline shrink-0 rotate-[145deg] animate-bounce"
23-
/>
24-
</div>
25-
</div>
26-
</div>
27-
);
3+
export default function Page() {
4+
return <PopupPageDynamic />;
285
}

components/embed-popup/standalone-bundle-root.tsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,36 @@ import { getAppConfig } from '@/lib/env';
44
import globalCss from '@/styles/globals.css';
55
import EmbedFixedAgentClient from './agent-client';
66

7-
const wrapper = document.createElement('div');
8-
wrapper.setAttribute('id', 'lk-embed-wrapper');
9-
document.body.appendChild(wrapper);
7+
const scriptTag = document.querySelector<HTMLScriptElement>('script[data-lk-sandbox-id]');
8+
const sandboxIdAttribute = scriptTag?.dataset.lkSandboxId;
109

11-
// Use a shadow root so that any relevant css classes don't leak out and effect the broader page
12-
const shadowRoot = wrapper.attachShadow({ mode: 'open' });
10+
if (sandboxIdAttribute) {
11+
const wrapper = document.createElement('div');
12+
wrapper.setAttribute('id', 'lk-embed-wrapper');
13+
document.body.appendChild(wrapper);
1314

14-
// Include all app styles into the shadow root
15-
// FIXME: this includes styles for the welcome page / etc, not just the popup embed!
16-
const styleTag = document.createElement('style');
17-
styleTag.textContent = globalCss;
18-
shadowRoot.appendChild(styleTag);
15+
// Use a shadow root so that any relevant css classes don't leak out and effect the broader page
16+
const shadowRoot = wrapper.attachShadow({ mode: 'open' });
1917

20-
const reactRoot = document.createElement('div');
21-
shadowRoot.appendChild(reactRoot);
18+
// Include all app styles into the shadow root
19+
// FIXME: this includes styles for the welcome page / etc, not just the popup embed!
20+
const styleTag = document.createElement('style');
21+
styleTag.textContent = globalCss;
22+
shadowRoot.appendChild(styleTag);
2223

23-
getAppConfig(window.location.origin)
24-
.then((appConfig) => {
25-
const root = ReactDOM.createRoot(reactRoot);
26-
root.render(<EmbedFixedAgentClient appConfig={appConfig} />);
27-
})
28-
.catch((err) => {
29-
console.error('Error loading livekit embed-popup app config:', err);
30-
});
24+
const reactRoot = document.createElement('div');
25+
shadowRoot.appendChild(reactRoot);
26+
27+
getAppConfig(window.location.origin, sandboxIdAttribute)
28+
.then((appConfig) => {
29+
const root = ReactDOM.createRoot(reactRoot);
30+
root.render(<EmbedFixedAgentClient appConfig={appConfig} />);
31+
})
32+
.catch((err) => {
33+
console.error('LiveKit popup embed error - Error loading app config:', err);
34+
});
35+
} else {
36+
console.error(
37+
'LiveKit popup embed error - no data-lk-sandbox-id attribute found on script tag. This is required!'
38+
);
39+
}

components/popup-page-dynamic.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use client';
2+
3+
import dynamic from 'next/dynamic';
4+
5+
export default dynamic(() => import('./popup-page'), { ssr: false });

components/popup-page.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client';
2+
3+
import { useMemo } from 'react';
4+
import Script from 'next/script';
5+
import { HandPointingIcon } from '@phosphor-icons/react';
6+
import { ThemeToggle } from '@/components/theme-toggle';
7+
import { getSandboxId } from '@/lib/env';
8+
9+
export default function PopupPage() {
10+
const sandboxId = useMemo(() => getSandboxId(window.location.origin), []);
11+
return (
12+
<div className="grid min-h-screen place-items-center">
13+
<Script src="/embed-popup.js" data-lk-sandbox-id={sandboxId} />
14+
<div className="space-y-10">
15+
<div className="flex justify-center">
16+
<ThemeToggle className="w-fit" />
17+
</div>
18+
<div className="text-fgAccent flex gap-1">
19+
<p className="grow text-sm">
20+
The popup button should appear in the bottom right corner of the screen
21+
</p>
22+
<HandPointingIcon
23+
size={16}
24+
weight="regular"
25+
className="mt-0.5 inline shrink-0 rotate-[145deg] animate-bounce"
26+
/>
27+
</div>
28+
</div>
29+
</div>
30+
);
31+
}

components/welcome.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
55
import { motion } from 'motion/react';
66
import { CheckIcon, CopyIcon, HandPointingIcon } from '@phosphor-icons/react';
77
import { APP_CONFIG_DEFAULTS } from '@/app-config';
8-
import { THEME_STORAGE_KEY } from '@/lib/env';
8+
import { THEME_STORAGE_KEY, getSandboxId } from '@/lib/env';
99
import type { ThemeMode } from '@/lib/types';
1010
import { cn } from '@/lib/utils';
1111
import EmbedPopupAgentClient from './embed-popup/agent-client';
@@ -42,9 +42,11 @@ export default function Welcome() {
4242
return url.toString();
4343
}, []);
4444

45+
const embedSandboxId = useMemo(() => getSandboxId(window.location.origin), []);
46+
4547
const popupEmbedCode = useMemo(
46-
() => `<script\n src="${popupEmbedUrl}"\n></script>`,
47-
[popupEmbedUrl]
48+
() => `<script\n src="${popupEmbedUrl}"\n data-lk-sandbox-id="${embedSandboxId}"\n></script>`,
49+
[popupEmbedUrl, embedSandboxId]
4850
);
4951
const iframeEmbedCode = useMemo(() => {
5052
return `<iframe\n src="${iframeEmbedUrl}"\n style="width: 320px; height: 64px;"\n></iframe>`;
@@ -165,13 +167,8 @@ export default function Welcome() {
165167
<h3 className="sr-only text-lg font-semibold">Popup Style</h3>
166168
<div>
167169
<h4 className="text-fg0 mb-1 font-semibold">Embed code</h4>
168-
<pre className="border-separator2 bg-bg2 relative overflow-auto rounded-md border px-2 py-1">
170+
<pre className="border-separator2 bg-bg2 overflow-auto rounded-md border px-2 py-1">
169171
<code className="font-mono">{popupEmbedCode}</code>
170-
<div className="absolute top-0 right-0">
171-
<Button onClick={() => copyEmbedCode(popupEmbedCode)}>
172-
{copied ? <CheckIcon className="text-fgSuccess" /> : <CopyIcon />}
173-
</Button>
174-
</div>
175172
</pre>
176173
<p className="text-fg4 my-4 text-sm">
177174
To apply local changes, run{' '}

lib/env.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,44 @@ export function getOrigin(headers: Headers): string {
1414
return `${proto}://${host}`;
1515
}
1616

17+
export function getSandboxId(origin: string) {
18+
return SANDBOX_ID ?? origin.split('.')[0];
19+
}
20+
1721
// https://react.dev/reference/react/cache#caveats
1822
// > React will invalidate the cache for all memoized functions for each server request.
19-
export const getAppConfig = cache(async (origin: string): Promise<AppConfig> => {
20-
if (CONFIG_ENDPOINT) {
21-
const sandboxId = SANDBOX_ID ?? origin.split('.')[0];
22-
23-
try {
24-
const response = await fetch(CONFIG_ENDPOINT, {
25-
cache: 'no-store',
26-
headers: { 'X-Sandbox-ID': sandboxId },
27-
});
28-
29-
const remoteConfig: SandboxConfig = await response.json();
30-
const config: AppConfig = { ...APP_CONFIG_DEFAULTS };
31-
32-
for (const [key, entry] of Object.entries(remoteConfig)) {
33-
if (entry === null) continue;
34-
if (
35-
key in config &&
36-
typeof config[key as keyof AppConfig] === entry.type &&
37-
typeof config[key as keyof AppConfig] === typeof entry.value
38-
) {
39-
// @ts-expect-error I'm not sure quite how to appease TypeScript, but we've thoroughly checked types above
40-
config[key as keyof AppConfig] = entry.value as AppConfig[keyof AppConfig];
23+
export const getAppConfig = cache(
24+
async (origin: string, sandboxIdAttribute?: string): Promise<AppConfig> => {
25+
if (CONFIG_ENDPOINT) {
26+
const sandboxId = sandboxIdAttribute ?? getSandboxId(origin);
27+
28+
try {
29+
const response = await fetch(CONFIG_ENDPOINT, {
30+
cache: 'no-store',
31+
headers: { 'X-Sandbox-ID': sandboxId },
32+
});
33+
34+
const remoteConfig: SandboxConfig = await response.json();
35+
const config: AppConfig = { ...APP_CONFIG_DEFAULTS };
36+
37+
for (const [key, entry] of Object.entries(remoteConfig)) {
38+
if (entry === null) continue;
39+
if (
40+
key in config &&
41+
typeof config[key as keyof AppConfig] === entry.type &&
42+
typeof config[key as keyof AppConfig] === typeof entry.value
43+
) {
44+
// @ts-expect-error I'm not sure quite how to appease TypeScript, but we've thoroughly checked types above
45+
config[key as keyof AppConfig] = entry.value as AppConfig[keyof AppConfig];
46+
}
4147
}
42-
}
4348

44-
return config;
45-
} catch (error) {
46-
console.error('!!!', error);
49+
return config;
50+
} catch (error) {
51+
console.error('!!!', error);
52+
}
4753
}
48-
}
4954

50-
return APP_CONFIG_DEFAULTS;
51-
});
55+
return APP_CONFIG_DEFAULTS;
56+
}
57+
);

0 commit comments

Comments
 (0)