ScaleBun
Skip to article

Engagement

webDeveloper

Surveys, NPS/CSAT, and in-app campaigns that fire from dashboard triggers, plus web push notifications and dashboard-authored coachmark tours.

Updated Reviewed

Engagement is on by default. Campaigns — surveys, NPS/CSAT, and in-app messages — fire automatically from triggers you configure in the dashboard, so most of this needs no code at all.

Surveys and campaigns#

Configure the audience, trigger, and content in the dashboard; the SDK evaluates triggers on the client and renders the surface. No per-campaign code is required.

Surveys from your own UI#

If you render the survey yourself, submit through the SDK so responses are retried and deduplicated:

TypeScript
await ScaleBun.engage.submit(surveyId, result);   // 'success' | 'error'await ScaleBun.engage.refresh();                  // re-read config, e.g. after loginScaleBun.engage.emitRatingEvent('shown');         // rating funnel eventsScaleBun.submitRating(5, 'Loved it');             // 1–5; invalid scores are dropped

For QA, ScaleBun.engage.resetInAppState() clears on-device frequency and dismissal state so campaigns show again — no incognito window needed.

Web push#

Web push is facade-lazy — just call the API. It uses a service worker and VAPID.

TypeScript
const status = await ScaleBun.enablePush();// 'subscribed' | 'unsupported' | 'not-configured' | 'denied' | 'error'
ScaleBun.onNotificationOpened(({ data }) => {  if (data.url) location.assign(String(data.url));});
await ScaleBun.push.status();await ScaleBun.push.getSubscription();await ScaleBun.push.disable();

ScaleBun.push.enable() is an alias of enablePush() — same call, whichever reads better next to the rest of your push code.

Either must be called from a user gesture — browsers only allow the permission prompt in response to one.

Ask reversibly first#

softAsk renders an in-page prompt and calls enablePush() from its Allow button — that click is the user gesture, so you do not have to wire one up.

TypeScript
await ScaleBun.init({  clientKey,  apiBaseUrl,  push: {    softAsk: {      trigger: { afterMs: 30000 },   // 'immediate' | { afterMs } | { onSelector }      title: 'Get notified about order updates?',      allowText: 'Yes, notify me',      denyText: 'Not now',      remindAfterDays: 14,    },  },});

This is protection, not politeness. A "Not now" here is reversible; a browser-level Block is permanent and the SDK cannot undo it. Chrome also demotes sites with poor accept rates to a near-invisible prompt — so one badly-timed direct ask can cost you the ability to ask at all.

Diagnosing push#

TypeScript
const report = await ScaleBun.push.check();// { ok, blocking, checks: [{ label, fix, … }] }

Never prompts and never throws, so it is safe to call from a settings screen. Each check carries its own label and fix text, so a UI can render the report generically.

In-app messaging#

There are two in-app systems and they are disjoint — a message is delivered by one or the other, never both, so they can run side by side.

Legacy in-app campaigns (part of engage, on by default)#

Four formats belong in the page rather than over it — inline_card, embedded_panel, empty_state_prompt and smart_inbox_message. The SDK cannot guess where, so register a slot whose name matches the campaign's placement key:

TypeScript
const unregister = await ScaleBun.registerInAppSlot('empty-state', el);// call unregister() on unmount

Without a matching slot those campaigns fall back to a modal, so registering one is purely additive. Call unregister() when the element unmounts — otherwise messages render into a detached node, which looks exactly like them not rendering at all.

In-App Messaging 2.0 (features.inapp2, off by default)#

The schema-driven engine: sixteen surfaces (modal, slide-over, panel, popover, coachmark, tour, hotspot, inline, banner, toast, center, launcher, palette, checklist, takeover, toolbar) and twelve trigger types. It renders a new authoring path that is separate from the legacy campaigns above.

TypeScript
await ScaleBun.init({ clientKey, apiBaseUrl, features: { inapp2: true } });

Rendering it yourself#

InAppHeadless is a standalone client for rendering a variant you already hold — custom UI, or a dashboard preview. It works without features.inapp2, because it renders what you hand it rather than fetching campaigns. It lives on the /inapp subpath so none of the engine reaches the size-gated core bundle:

TypeScript
import { InAppHeadless } from '@scalebun/web/inapp';
const inapp = new InAppHeadless();inapp.setTheme('auto');            // 'auto' | 'light' | 'dark'inapp.registerSlot('empty-state', el);await inapp.render(variant);       // hooks optional — defaults to no-ops

Coachmarks#

Dashboard-authored guided tours anchor to elements on the page, and each step is replay-coordinate stamped so you can see how users move through a tour.

Next#

Engagement · Web SDK · ScaleBun