B Bubbl Docs

Notifications, CTAs, Media, and Surveys

Bubbl campaigns can include notifications, media, calls to action, and surveys. You can use Bubbl's default UI or render campaign content in your own app UI.

This page explains what each layer controls:

Layer Controls
Bubbl dashboard Campaign content, notification copy, media, CTA labels/URLs/actions, survey questions, campaign timing, geofences, and segment eligibility.
Bubbl SDK Payload parsing, token registration, default notification/detail UI, default survey UI, interaction tracking, event queueing, and diagnostics.
Your app Permission prompts, Firebase/APNs setup, foreground/background lifecycle, host-rendered UI, app navigation, theming decisions, and accessibility checks.

Rendering modes

The SDK supports three notification rendering approaches:

Mode Use when
SDK default You want Bubbl to show the default notification/modal experience.
Host rendered Your app wants to render the UI but still use Bubbl parsing and tracking.
Event only Your app wants SDK events only and will handle presentation separately.

Set the mode in config:

BubblConfig(
    apiKey = "<YOUR_BUBBL_API_KEY>",
    environment = BubblEnvironment.Production,
    notificationRenderingMode = BubblNotificationRenderingMode.SdkDefault,
    enableDefaultNotificationModal = true,
    enableDefaultSurveyUi = true
)

Default modal and survey UI

The SDK can present the default notification modal and default survey UI. Use this when you want the fastest implementation and your app does not need to fully control the notification detail screen.

BubblConfig(
    apiKey = "<YOUR_BUBBL_API_KEY>",
    environment = BubblEnvironment.Production,
    enableDefaultNotificationModal = true,
    enableDefaultSurveyUi = true
)

You can customize supported colors and corner radius with defaultNotificationModalStyle. The style affects the SDK default notification detail modal and the default survey UI that opens from that modal.

Supported style fields:

Field Use
theme Light or dark defaults for unspecified colors.
transparentBackdrop Whether the modal background lets your app show through behind it.
backdropColor Backdrop color when you want an explicit overlay color.
cardBackgroundColor and cardBorderColor Modal surface and border colors.
titleColor, bodyColor, and accentColor Main text and accent colors.
iconBackgroundColor and iconTextColor Icon/chip treatment where the platform renders one.
primaryButtonBackgroundColor and primaryButtonTextColor Primary CTA button colors.
secondaryButtonBackgroundColor and secondaryButtonTextColor Secondary action button colors.
textButtonColor Text-style action color.
surveyBackgroundColor Survey section background color.
inputBackgroundColor, inputTextColor, and inputBorderColor Survey text input styling where applicable.
cornerRadius and buttonCornerRadius Modal and button corner radius.

Keep enough contrast for titles, body copy, CTAs, and survey options. Test long campaign titles, long CTA labels, media content, and device font scaling before release.

Android:

BubblSdk.setDefaultNotificationModalStyle(
    BubblNotificationModalStyle(
        theme = BubblNotificationModalTheme.Light,
        cardBackgroundColor = "#FFFFFF",
        titleColor = "#101828",
        bodyColor = "#344054",
        accentColor = "#2563EB",
        primaryButtonBackgroundColor = "#2563EB",
        primaryButtonTextColor = "#FFFFFF",
        cornerRadius = 16.0,
        buttonCornerRadius = 12.0
    )
)

iOS:

try await BubblClient.shared.setDefaultNotificationModalStyle(
    BubblNotificationModalStyle(
        theme: .light,
        cardBackgroundColor: "#FFFFFF",
        titleColor: "#101828",
        bodyColor: "#344054",
        accentColor: "#2563EB",
        primaryButtonBackgroundColor: "#2563EB",
        primaryButtonTextColor: "#FFFFFF",
        cornerRadius: 16,
        buttonCornerRadius: 12
    )
)

Flutter:

await BubblSdk.instance.setDefaultNotificationModalStyle(
  const BubblNotificationModalStyle(
    theme: BubblNotificationModalTheme.light,
    cardBackgroundColor: '#FFFFFF',
    titleColor: '#101828',
    bodyColor: '#344054',
    accentColor: '#2563EB',
    primaryButtonBackgroundColor: '#2563EB',
    primaryButtonTextColor: '#FFFFFF',
    cornerRadius: 16,
    buttonCornerRadius: 12,
  ),
);

React Native:

await Bubbl.setDefaultNotificationModalStyle({
  theme: 'light',
  cardBackgroundColor: '#FFFFFF',
  titleColor: '#101828',
  bodyColor: '#344054',
  accentColor: '#2563EB',
  primaryButtonBackgroundColor: '#2563EB',
  primaryButtonTextColor: '#FFFFFF',
  cornerRadius: 16,
  buttonCornerRadius: 12,
});

You can also provide the style during boot if your app always uses the same styling:

Flutter:

await BubblSdk.instance.boot(
  const BubblConfig(
    apiKey: String.fromEnvironment('BUBBL_API_KEY'),
    environment: BubblEnvironment.production,
    notificationRenderingMode: BubblNotificationRenderingMode.sdkDefault,
    enableDefaultNotificationModal: true,
    enableDefaultSurveyUi: true,
    defaultNotificationModalStyle: BubblNotificationModalStyle(
      theme: BubblNotificationModalTheme.light,
      accentColor: '#2563EB',
      primaryButtonBackgroundColor: '#2563EB',
      primaryButtonTextColor: '#FFFFFF',
      cornerRadius: 16,
      buttonCornerRadius: 12,
    ),
  ),
);

React Native:

await Bubbl.boot({
  apiKey: Config.BUBBL_API_KEY,
  environment: 'production',
  notificationRenderingMode: 'sdkDefault',
  enableDefaultNotificationModal: true,
  enableDefaultSurveyUi: true,
  defaultNotificationModalStyle: {
    theme: 'light',
    accentColor: '#2563EB',
    primaryButtonBackgroundColor: '#2563EB',
    primaryButtonTextColor: '#FFFFFF',
    cornerRadius: 16,
    buttonCornerRadius: 12,
  },
});

Reset the style by setting it to null where your wrapper exposes that helper, using the wrapper reset helper where available, or by setting the next style you want before opening a modal.

Opening the default modal from your app

If your app has an inbox, notification center, booking detail page, or campaign history, you can open the SDK default modal for a parsed payload without sending another device notification.

Flutter:

final opened = await BubblSdk.instance.openNotificationModal(payload);

React Native:

const opened = await Bubbl.openNotificationModal(payload);

iOS:

let opened = try await BubblClient.shared.openNotificationModal(payload)

Android:

val opened = BubblSdk.openNotificationModal(context, payload)

If opened is false, keep your app UI stable and capture diagnostics. A modal may fail to open if the app has no visible foreground view or activity.

Use this pattern for:

  • A notification inbox row that should show the same campaign detail as the original push.
  • A booking, journey, venue, or offer detail page that includes a Bubbl campaign.
  • A support flow where the user needs to reopen campaign media, CTA, or survey content.

Do not use openNotificationModal to create a new campaign. It opens a payload your app already received or loaded.

Host-rendered interactions

If your app renders its own UI, configure host-rendered mode and forward user actions back to the SDK so Bubbl can track them.

React Native:

await Bubbl.boot({
  apiKey: Config.BUBBL_API_KEY,
  environment: 'production',
  notificationRenderingMode: 'hostRendered',
  enableDefaultNotificationModal: false,
});

Flutter:

await BubblSdk.instance.boot(
  const BubblConfig(
    apiKey: String.fromEnvironment('BUBBL_API_KEY'),
    environment: BubblEnvironment.production,
    notificationRenderingMode: BubblNotificationRenderingMode.hostRendered,
    enableDefaultNotificationModal: false,
  ),
);

Common actions:

  • Notification opened.
  • CTA tapped.
  • Media viewed.
  • Survey requested.
  • Survey response submitted.

Host-rendered payload shape:

type BubblNotificationPayload = {
  id: string;
  title: string;
  body: string;
  locationId?: string | null;
  curatedNotificationId?: string | null;
  correlationId?: string | null;
  media?: {
    url: string;
    type?: string | null;
    altText?: string | null;
  } | null;
  cta?: {
    label: string;
    url?: string | null;
    action?: string | null;
  } | null;
  survey?: {
    questions?: Array<{
      id: string;
      title: string;
      type: string;
      choices?: Array<{ id: string; label: string }>;
    }>;
  } | null;
};

Treat optional fields as genuinely optional. A campaign can be notification-only, notification plus CTA, notification plus media, notification plus survey, or a combination.

Example host-rendered card flow:

const payload = await Bubbl.handleFirebasePayload(data);

if (!payload) {
  return;
}

renderCampaignCard({
  title: payload.title,
  body: payload.body,
  mediaUrl: payload.media?.url,
  ctaLabel: payload.cta?.label,
});

await Bubbl.handleNotificationOpen(payload, 'card_opened');

if (payload.media) {
  await Bubbl.handleNotificationMediaViewed(payload);
}

if (payload.cta?.label) {
  await Bubbl.handleNotificationCta(payload, payload.cta.action ?? 'primary_cta');
  openUrlOrRoute(payload.cta.url, payload.cta.action);
}

if (payload.survey?.questions?.length) {
  await Bubbl.handleNotificationSurveyRequested(payload);
  openSurveySheet(payload);
}

In a host-rendered modal, call handleNotificationOpen once when the user opens the campaign detail. Call handleNotificationCta when they actually tap the CTA, not when the button is merely visible. Call handleNotificationMediaViewed when media is visible enough to count in your product, for example when an image loads or a video screen opens.

Example survey response from custom UI:

BubblSdk.submitSurveyResponse(
    BubblSurveyResponse(
        curatedNotificationId = "<NOTIFICATION_ID>",
        answers = listOf(
            BubblSurveyAnswer(
                questionId = "<QUESTION_ID>",
                type = "single_choice",
                choiceIds = listOf("<CHOICE_ID>")
            )
        )
    )
)

Flutter:

await BubblSdk.instance.submitSurveyResponse(
  BubblSurveyResponse(
    curatedNotificationId: payload.curatedNotificationId!,
    locationId: payload.locationId,
    answers: [
      BubblSurveyAnswer(
        questionId: question.id,
        type: 'single_choice',
        choiceIds: [choice.id],
      ),
    ],
  ),
);

React Native:

await Bubbl.submitSurveyResponse({
  curatedNotificationId: payload.curatedNotificationId!,
  locationId: payload.locationId,
  answers: [
    {
      questionId: question.id,
      type: 'single_choice',
      choiceIds: [choice.id],
    },
  ],
});

For free-text or rating-style survey questions, send the answer value in the field your wrapper exposes for the question type. For choice questions, send the selected choice IDs from the payload. Do not make up question IDs or choice IDs in the app; use the IDs supplied by Bubbl.

Manual notification display

If you receive or build a Bubbl notification payload in your app, you can ask the SDK to display it:

val result = BubblSdk.showNotification(payload)

Flutter:

final result = await BubblSdk.instance.showNotification(payload);

React Native:

const result = await Bubbl.showNotification(payload);

If result.displayed is false, check result.reason, notification permission, foreground/background state, rendering mode, and diagnostics.

Manual display is useful for QA tools, an internal campaign preview, or a foreground message path where your app receives a Bubbl payload and wants the SDK to render it immediately. For normal background delivery, use the platform push flow.

CTA and survey guardrails

Use these checks before release:

  • Test a notification with no CTA, a notification with one CTA, and a notification with media plus CTA.
  • Do not send a CTA with an empty label. If there is no CTA, omit the CTA fields.
  • Make sure CTA URLs or app routes are valid on both iOS and Android.
  • Verify the default modal and any host-rendered modal with large text, screen readers, and reduced motion settings where applicable.
  • Confirm foreground behavior and background/tap behavior separately. Mobile operating systems handle those paths differently.
  • Confirm survey questions have stable IDs and answer options have stable choice IDs before you submit responses from custom UI.
  • Capture diagnostics after a notification is received, after the user opens it, and after survey submission.

Practical examples

Retail or venue offer:

  • Dashboard controls the offer title, image, geofence, CTA, and segment targeting.
  • Your app asks for location permission when the user enables nearby offers.
  • The SDK evaluates geofences and parses the campaign payload.
  • Your app either lets the SDK show the default modal or renders the offer in your own venue screen.

Transport or customer assistance:

  • Your app passes location updates during an active journey.
  • Bubbl sends service or assistance notifications when the user reaches a relevant location.
  • Your app can use correlationId to associate the notification with a booking, trip, or support case.

Loyalty and segmentation:

  • Your app updates segments after sign-in or tier changes.
  • Bubbl campaign eligibility uses those segment tags.
  • Your app should clear the correlation ID and replace segments when the user signs out.

Feedback survey:

  • Dashboard controls the survey questions and choices.
  • The SDK default survey UI can submit answers for you.
  • If your app renders the survey, submit the selected answers with submitSurveyResponse.

Silent analytics and correlation:

  • Use setCorrelationId for your own journey reference.
  • Use track only for supported app milestones you want Bubbl to receive.
  • Avoid putting unnecessary personal data in segment tags, correlation IDs, or custom event fields.

Choosing the right UI path

Use SDK default when:

  • You want the shortest route to a working notification, detail modal, media view, CTA, and survey.
  • Your app does not need a fully bespoke campaign screen.
  • You want Bubbl to handle most interaction tracking automatically.

Use SDK default with custom modal style when:

  • You want the default behavior but need the modal to match your app colors and radius.
  • Your design changes can be expressed through colors and simple shape options.

Use host-rendered UI when:

  • Campaign content must live inside an existing app surface such as an inbox, booking detail, map sheet, or venue page.
  • Your app needs custom navigation, custom media handling, or custom accessibility behavior.
  • Your app team is comfortable forwarding every meaningful interaction back to the SDK.

Use event-only mode when:

  • Your app wants to observe Bubbl payloads and events but owns the whole notification and presentation layer.
  • You have an existing notification framework that should remain the only UI path.