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
correlationIdto 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
setCorrelationIdfor your own journey reference. - Use
trackonly 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.