React & SPA integration
A plain <script> tag works on any website, including SPAs. You only need this page if you want more control than that — showing the widget to signed-in users only, removing it on logout, or keeping your tenant and key in environment variables instead of hard-coded in a template.
The pattern below is React, and the idea transfers unchanged to Vue, Angular, or Svelte: mount the embed script once, tear it down on unmount.
Dashboard → Widget → Install has a React / SPA tab that generates this component pre-filled with your own tenant, key, and host. Use Copy component there rather than retyping the placeholders below.
1. The component
// PromptlyWidget.jsx — mounts the Promptly chat widget in a React SPA.
'use client'; // Next.js App Router only — harmless elsewhere
import { useEffect } from 'react';
const SCRIPT_ID = 'promptly-embed-script';
export default function PromptlyWidget({ enabled = true }) {
useEffect(() => {
if (!enabled) return undefined;
if (window.__promptlyLoaded || document.getElementById(SCRIPT_ID)) {
return undefined;
}
const script = document.createElement('script');
script.id = SCRIPT_ID;
script.src = 'https://api.promptly-assistant.com/widget/embed.js';
script.async = true;
script.setAttribute('data-tenant', 'your-tenant-slug');
script.setAttribute('data-api-key', 'pk_your-widget-key');
script.setAttribute('data-api-base', 'https://api.promptly-assistant.com');
script.setAttribute('data-widget-base', 'https://api.promptly-assistant.com/widget');
document.body.appendChild(script);
return () => {
// The embed script has no destroy API — remove its DOM host and reset
// the init guard so the widget can mount again after the next login.
document.getElementById(SCRIPT_ID)?.remove();
document.getElementById('promptly-widget-host')?.remove();
delete window.__promptlyLoaded;
delete window.Promptly;
};
}, [enabled]);
return null;
}What it's doing, and why each part matters:
- The
__promptlyLoadedguard is set by the embed script itself. Checking it means React 18's development-mode double-mount, or two copies of the component rendered by mistake, can't produce two launchers. - The cleanup removes the Shadow DOM host and clears the guard, because there's no
destroy()to call. Skip the guard reset and the widget won't come back after a logout/login cycle. - It renders
null, so it can sit anywhere in your tree without affecting layout. The widget positions itself.
2. Render it once, near the root
import PromptlyWidget from './components/PromptlyWidget';
function App() {
return (
<>
<YourRoutes />
<PromptlyWidget />
</>
);
}Near the root, not inside a route — otherwise the widget unmounts and remounts on every navigation, and each remount throws away the open conversation.
3. Apps behind a login
Pass your auth state through:
<PromptlyWidget enabled={isLoggedIn} />The widget appears after login and is removed on logout, so it never shows on your sign-in screen.
4. Opening it from your own UI
Once loaded, the embed exposes a global you can call — useful when you already have a "Help" item in a menu and don't want a second entry point floating over it:
<button onClick={() => window.Promptly?.open()}>Chat with us</button>open, close, toggle, expand, collapse, and setColor are all available. Guard with ?. — the script loads asynchronously, so the global may not exist on the first render.
5. Allowed domains, including your dev server
At Widget → Install → Allowed Domains, add every origin the app runs on. Requests from anywhere else are rejected, which in a browser looks identical to a broken install.
- Production Domains tab — your real hostnames.
*.example.comcovers subdomains, so one entry can serve production and preview deployments. - Local Testing tab —
localhost:5173for Vite,localhost:3000for Next.js or CRA. Entering plainlocalhostwith no port allows every port, which is the least annoying option while you're developing.
Embedding your help center too
If you also embed your Promptly documentation inside the app, set Documentation → Settings → Embed → "Help center URL on your site" to the page hosting that iframe. Chat answers that cite a documentation page will then deep-link to your own page (?article=<slug>) instead of opening the hosted help center and dropping the visitor out of your app.
Troubleshooting
| Symptom | Cause |
|---|---|
| Widget shows on the login page | It's rendered unconditionally — pass enabled={isLoggedIn}. |
| Nothing appears after login | Check the console for a blocked-origin warning, then verify the origin is in Allowed Domains. |
| Widget stays after logout | The component isn't actually unmounting and isn't receiving enabled={false}. Cleanup runs in either case, so make sure one of them happens. |
| Two launchers | Two copies are mounted and one bypassed the guard — render <PromptlyWidget /> in exactly one place. |
| Conversation resets on navigation | The component is mounted inside a route. Move it above the router. |
window.Promptly is undefined | The script hasn't finished loading. Use optional chaining, or wire the button after first paint. |
One caveat on cleanup: unmounting removes the widget's DOM and resets the guard, but a few document-level listeners the embed script registered stay behind. They're inert without the host element and are cleared by the next full page load — worth knowing if you're auditing listener counts, harmless otherwise.