We've shipped this on iOS with a WKWebView and it works well enough. The approach, in case it's useful:
There's no URL that renders the messenger, so instead of loading a page we host one. The app builds a blank HTML page and loads it with loadHTMLString(html, baseURL:) — the base URL matters, it gives the SDK a normal origin to make its requests from. That page loads sdk.js, boots, and opens the messenger:
Featurebase('boot', { appId, theme, language: 'en', hideDefaultLauncher: true });
// boot resolves async and `show` only warns if it lands too early,// so poll for the messenger frame, then open it.const timer = setInterval(() => {
if (!registered && document.querySelector('.featurebase-messenger-frame')) {
registered = true;
Featurebase('onShow', () => { shown = true; post('shown'); });
Featurebase('onHide', () =>post('closed')); // dismiss the native screenFeaturebase('onExternalLinkOpen', u =>post('link', u)); // open in Safari
}
if (registered && !shown) Featurebase('show', 'help');
if (shown) clearInterval(timer);
}, 250);
post() is just window.webkit.messageHandlers.…postMessage, which gives the native side the hooks it needs — close the screen when the user closes the messenger, hand links to the system, show a spinner until onShow fires.
At phone width the messenger takes over the whole viewport and honestly looks native. Four things that cost us time, which an official version would handle for people:
- The panel is translucent, so whatever is behind it tints the entire widget. Our app theme is dark navy and it bled straight through. The host page needs a solid neutral background, and the web view has to be opaque in the same colour.
- The messenger CSS doesn't use env(safe-area-inset-*), so the web view has to stay inside the safe area — full-bleed puts the header and ✕ under the status bar and the tab bar under the home indicator.
- Main-frame navigations have to be cancelled, or a link replaces the page the widget is living on and takes the widget with it.
- Use the persistent data store so the anonymous id survives reopening the screen.
The fragile part is that we're keying on .featurebase-messenger-frame, an internal class name, to know when the messenger is ready — one refactor on your side and every integration doing this breaks silently. An official host page, or a thin iOS/Android SDK wrapping exactly this, would fix that and give you a ready-made mobile story.
HAT Music
•
Jan 2
Probably the most useful feature
Marcus Fütö
•
Dec 4, 2025
basic
Plancana Team
•
Aug 13, 2025
This is a killer feature for any mobile developer. We love the concert of Featurebase and would love to integrate the Messenger SDK into our app.
Rahul
•
Jul 18, 2025
WE NEED THIS!
David Oort Alonso
•
Jul 18, 2025
This is super important for us and kinda blocking our launch. We promise premium customer support to paid users and on mobile we’re not able to provide this experience
Sondre from Tings
•
Jun 6, 2025
Will the current iOS widget – and the “/widget” urls for web views – continue working until this is ready?
Log in to comment and vote
Comments7
Andrew
Aug 31
We've shipped this on iOS with a
WKWebViewand it works well enough. The approach, in case it's useful:There's no URL that renders the messenger, so instead of loading a page we host one. The app builds a blank HTML page and loads it with
loadHTMLString(html, baseURL:)— the base URL matters, it gives the SDK a normal origin to make its requests from. That page loadssdk.js, boots, and opens the messenger:Featurebase('boot', { appId, theme, language: 'en', hideDefaultLauncher: true }); // boot resolves async and `show` only warns if it lands too early, // so poll for the messenger frame, then open it. const timer = setInterval(() => { if (!registered && document.querySelector('.featurebase-messenger-frame')) { registered = true; Featurebase('onShow', () => { shown = true; post('shown'); }); Featurebase('onHide', () => post('closed')); // dismiss the native screen Featurebase('onExternalLinkOpen', u => post('link', u)); // open in Safari } if (registered && !shown) Featurebase('show', 'help'); if (shown) clearInterval(timer); }, 250);post()is justwindow.webkit.messageHandlers.…postMessage, which gives the native side the hooks it needs — close the screen when the user closes the messenger, hand links to the system, show a spinner untilonShowfires.At phone width the messenger takes over the whole viewport and honestly looks native. Four things that cost us time, which an official version would handle for people:
- The panel is translucent, so whatever is behind it tints the entire widget. Our app theme is dark navy and it bled straight through. The host page needs a solid neutral background, and the web view has to be opaque in the same colour.
- The messenger CSS doesn't use
env(safe-area-inset-*), so the web view has to stay inside the safe area — full-bleed puts the header and ✕ under the status bar and the tab bar under the home indicator.- Main-frame navigations have to be cancelled, or a link replaces the page the widget is living on and takes the widget with it.
- Use the persistent data store so the anonymous id survives reopening the screen.
The fragile part is that we're keying on
.featurebase-messenger-frame, an internal class name, to know when the messenger is ready — one refactor on your side and every integration doing this breaks silently. An official host page, or a thin iOS/Android SDK wrapping exactly this, would fix that and give you a ready-made mobile story.HAT Music
Jan 2
Probably the most useful feature
Marcus Fütö
Dec 4, 2025
basic
Plancana Team
Aug 13, 2025
This is a killer feature for any mobile developer. We love the concert of Featurebase and would love to integrate the Messenger SDK into our app.
Rahul
Jul 18, 2025
WE NEED THIS!
David Oort Alonso
Jul 18, 2025
This is super important for us and kinda blocking our launch. We promise premium customer support to paid users and on mobile we’re not able to provide this experience
Sondre from Tings
Jun 6, 2025
Will the current iOS widget – and the “/widget” urls for web views – continue working until this is ready?