Migration · iOS · Android · React Native · Flutter
Migrating from 4.x to 5.x
A clean break: one start call, automatic push, location and taps, and new names across the API. No 4.x data carries over, and each install registers as a new device.
0 of 19 done
Update the dependency, and move iOS to Swift Package ManagerBump Bubbl to 5.x on every platform. On iOS, Bubbl 5 comes through Swift Package Manager only, not CocoaPods: remove
Breakingpod 'BubblSDK' (and the Bubbl-Sdk alias, if you used it) from your Podfile, then in Xcode choose File › Add Package Dependencies…, enter https://github.com/bubbl-public/bubbl-ios-sdk and add the BubblSDK product to your app target. Android builds with compileSdk 35. The Flutter plugin needs Flutter 3.32 or later and carries the native SDKs itself, so there's no pod, Swift package or Gradle dependency to add. The React Native package needs React Native 0.76 or later with the New Architecture on, and builds the iOS SDK into its own pod.BEFORE · 4.x
// app/build.gradle.kts
dependencies {
implementation("tech.bubbl.sdk:bubbl-sdk:4.1.7")
}# Podfile
target 'MyApp' do
pod 'BubblSDK', '4.1.7'
end# pubspec.yaml
dependencies:
bubbl_flutter_sdk: ^4.1.7npm install @bubblsdk/react-native-sdk@4.1.7
cd ios && pod installAFTER · 5.x
// app/build.gradle.kts
android {
compileSdk = 35
}
dependencies {
implementation("tech.bubbl.sdk:bubbl-sdk:5.0.0")
}// Xcode: File › Add Package Dependencies…, then add the BubblSDK product to your app target.
// Or, in a Package.swift:
.package(url: "https://github.com/bubbl-public/bubbl-ios-sdk", exact: "5.0.0")# pubspec.yaml
dependencies:
bubbl_flutter_sdk: ^5.0.0npm install @bubblsdk/react-native-sdk@5.0.0
cd ios && pod installStart Bubbl with one call4.x's
Renamedinstall and boot(BubblConfig(…)) become a single start call with your API key and a BubblOptions. Call it early in every launch: in Application.onCreate on Android (not in an activity, because a background wake-up for a geofence or a push runs only Application.onCreate), at launch on iOS, in main() on Flutter, and at the top of index.js on React Native. start returns no boot result. A mistake such as an empty key is logged, not thrown, except on Android, where start throws IllegalArgumentException. Bubbl gets each install its own device credential from the API key. Only a phone paired with a code (like Bubbl's Showcase app) starts with a credential instead: start(context, credential, options) on Android, start(credential:options:) on iOS, and startWithCredential on Flutter and React Native.BEFORE · 4.x
class App : Application() {
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onCreate() {
super.onCreate()
BubblSdk.install(this)
appScope.launch {
BubblSdk.boot(BubblConfig(apiKey = "…"))
}
}
}func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Task {
_ = try await BubblClient.shared.boot(BubblConfig(apiKey: "…"))
}
return true
}Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await BubblSdk.instance.boot(const BubblConfig(apiKey: '…'));
runApp(const MyApp());
}import { Bubbl } from '@bubblsdk/react-native-sdk';
Bubbl.boot({ apiKey: '…' });AFTER · 5.x
class App : Application() {
override fun onCreate() {
super.onCreate()
Bubbl.start(this, "pk_test_…", BubblOptions(baseUrl = "https://api.sandbox.bubbl.tech"))
}
}func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Bubbl.start(apiKey: "pk_test_…", options: BubblOptions(baseUrl: "https://api.sandbox.bubbl.tech"))
return true
}Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Bubbl.start(
apiKey: 'pk_test_…',
options: const BubblOptions(baseUrl: 'https://api.sandbox.bubbl.tech'),
);
runApp(const MyApp());
}// index.js
import { Bubbl } from '@bubblsdk/react-native-sdk';
Bubbl.start('pk_test_…', { baseUrl: 'https://api.sandbox.bubbl.tech' });Pass one base URL4.x chose its hosts from
Breakingenvironment (development, staging or production) and the runtimeBaseUrl, transmissionBaseUrl and ingestBaseUrl overrides. 5.x has a single, required baseUrl and no default host. Pass the host root, https://api.sandbox.bubbl.tech for Sandbox or https://api.bubbl.tech for Production, without /api/v1, which the SDK adds itself. It must be https://; plain http:// is accepted only for local development hosts such as localhost and .test names.BEFORE · 4.x
BubblSdk.boot(
BubblConfig(
apiKey = "…",
environment = BubblEnvironment.Production,
// or runtimeBaseUrl / transmissionBaseUrl / ingestBaseUrl overrides
)
)_ = try await BubblClient.shared.boot(
BubblConfig(apiKey: "…", environment: .production)
// or runtimeBaseUrl / transmissionBaseUrl / ingestBaseUrl overrides
)await BubblSdk.instance.boot(
const BubblConfig(apiKey: '…', environment: BubblEnvironment.production),
// or runtimeBaseUrl / transmissionBaseUrl / ingestBaseUrl overrides
);Bubbl.boot({ apiKey: '…', environment: 'production' });
// or runtimeBaseUrl / transmissionBaseUrl / ingestBaseUrl overridesAFTER · 5.x
Bubbl.start(this, "pk_live_…", BubblOptions(baseUrl = "https://api.bubbl.tech"))Bubbl.start(apiKey: "pk_live_…", options: BubblOptions(baseUrl: "https://api.bubbl.tech"))await Bubbl.start(
apiKey: 'pk_live_…',
options: const BubblOptions(baseUrl: 'https://api.bubbl.tech'),
);Bubbl.start('pk_live_…', { baseUrl: 'https://api.bubbl.tech' });Remove location tracking callsGeofencing in 5.x is driven by the operating system and starts on its own once the app has location permission, including with the app closed. Remove
RemovedenableLocationTracking, startLocationTracking() and stopLocationTracking(), the iOS BubblLocationMonitor, the React Native BubblSdkLocationLaunchHandler.handleLaunchOptions call in your AppDelegate, and any refreshGeofence, handleLocationUpdate, refresh or flush calls: there's no way to feed locations in by hand any more. On Android, 4.x's foreground location service is gone too. To turn location off for a user, or back on, call setLocationEnabled.BEFORE · 4.x
lifecycleScope.launch {
BubblSdk.boot(BubblConfig(apiKey = "…", enableLocationTracking = true))
BubblSdk.startLocationTracking()
}
// later
BubblSdk.stopLocationTracking()let monitor = BubblLocationMonitor()
monitor.requestAlwaysAuthorization()
monitor.start()
// later
monitor.stop()await BubblSdk.instance.boot(
const BubblConfig(apiKey: '…', enableLocationTracking: true),
);
await BubblSdk.instance.startLocationTracking();
// later
await BubblSdk.instance.stopLocationTracking();await Bubbl.boot({ apiKey: '…', enableLocationTracking: true });
await Bubbl.startLocationTracking();
// AppDelegate.swift: BubblSdkLocationLaunchHandler.handleLaunchOptions(launchOptions as NSDictionary?)AFTER · 5.x
// Nothing to start. To turn geofences off, and back on:
Bubbl.setLocationEnabled(false)
Bubbl.setLocationEnabled(true)// Nothing to start. To turn geofences off, and back on:
Bubbl.setLocationEnabled(false)
Bubbl.setLocationEnabled(true)// Nothing to start. To turn geofences off, and back on:
await Bubbl.setLocationEnabled(false);
await Bubbl.setLocationEnabled(true);// Nothing to start. To turn geofences off, and back on:
Bubbl.setLocationEnabled(false);
Bubbl.setLocationEnabled(true);Stop forwarding pushes and tokensBubbl 5 receives its own pushes and reads the push token itself, so remove
RemovedregisterPushToken, syncFcmToken, handleFirebasePayload, updateAPNsToken, updateFCMToken, handleRemoteNotification and BubblNotificationCenterDelegate.installDefault(). On Android, Bubbl listens beside any FirebaseMessagingService of yours (4.x's BubblFirebaseMessagingService is gone), and its pushes still reach your onMessageReceived, Flutter's firebase_messaging and React Native Firebase handlers, so skip them there with isBubblMessage. On iOS, Bubbl now sends straight to Apple (APNs), without Firebase: add the Push Notifications capability and your APNs key under Configuration › Apple Push in the dashboard, and Bubbl asks iOS for the device token once notifications are allowed and hooks into whichever notification delegate is set, answering only for its own pushes. Don't call registerForRemoteNotifications() for Bubbl's sake.BEFORE · 4.x
class MyMessagingService : FirebaseMessagingService() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMessageReceived(message: RemoteMessage) {
scope.launch {
BubblSdk.handleFirebasePayload(
payload = message.data,
messageId = message.messageId,
notificationTitle = message.notification?.title,
notificationBody = message.notification?.body
)
}
}
override fun onNewToken(token: String) {
scope.launch { BubblSdk.syncFcmToken(token) }
}
}// At launch
BubblNotificationCenterDelegate.installDefault()
application.registerForRemoteNotifications()
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Task { try? await BubblClient.shared.updateAPNsToken(deviceToken) }
}
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Task {
_ = try? await BubblClient.shared.handleRemoteNotification(userInfo)
completionHandler(.newData)
}
}FirebaseMessaging.instance.onTokenRefresh.listen(BubblSdk.instance.registerPushToken);
FirebaseMessaging.onMessage.listen((message) {
BubblSdk.instance.handleFirebasePayload(Map<String, String>.from(message.data));
});import messaging from '@react-native-firebase/messaging';
messaging().onTokenRefresh((token) => Bubbl.registerPushToken(token));
messaging().onMessage(async (remoteMessage) => {
await Bubbl.handleFirebasePayload(remoteMessage.data as Record<string, string>);
});AFTER · 5.x
class MyMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
if (Bubbl.isBubblMessage(message.data)) return
// your own pushes
}
// No onNewToken forwarding: Bubbl reads the FCM token itself.
}// Nothing to forward: Bubbl picks up the APNs token and its own pushes by itself.
// Where your own code needs to tell Bubbl's payloads apart:
if Bubbl.isBubblMessage(userInfo) { return }FirebaseMessaging.onMessage.listen((message) {
if (Bubbl.isBubblMessage(message.data)) return;
// your own pushes
});import messaging from '@react-native-firebase/messaging';
messaging().onMessage(async (remoteMessage) => {
if (Bubbl.isBubblMessage(remoteMessage.data)) return;
// your own pushes
});Drop notification tap forwardingA tap on one of Bubbl's notifications now opens Bubbl's screen directly, so there's no launcher intent to forward and no pending taps to drain. Remove
RemovedopenNotificationIntent and BubblNotificationTapPresentation from your launcher activity, BubblSdkNotificationIntents from a React Native MainActivity, and any drainPendingNotificationTaps() calls. To react to a tap yourself, listen for the notification-opened event, which replaces 4.x's notification-tapped event.BEFORE · 4.x
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
BubblSdk.openNotificationIntent(this, intent, BubblNotificationTapPresentation.DefaultModal)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
BubblSdk.openNotificationIntent(this, intent, BubblNotificationTapPresentation.DefaultModal)
}
}Task {
for await event in await BubblClient.shared.events {
if case let .notificationTapped(payload, _) = event {
// open your screen for payload
}
}
}BubblSdk.instance.events.listen((event) {
if (event is BubblNotificationTappedEvent) {
// open your screen for event.payload
}
});// MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
BubblSdkNotificationIntents.openDefaultModal(this, intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
BubblSdkNotificationIntents.openDefaultModal(this, intent)
}AFTER · 5.x
// Nothing in MainActivity. To react to a tap:
Bubbl.addEventListener { event ->
when (event) {
is BubblEvent.NotificationOpened -> { /* event.message */ }
else -> Unit
}
}let token = Bubbl.addEventListener { event in
if case let .notificationOpened(message) = event {
// message.id, message.headline…
}
}Bubbl.addEventListener((event) {
if (event is BubblNotificationOpened) {
// event.message
}
});// Nothing in MainActivity. To react to a tap:
Bubbl.addEventListener((event) => {
if (event.type === 'notification.opened') {
// event.message
}
});Set segments with setSegments
RenamedupdateSegments(tags) is now setSegments(segments). It still replaces any segments set before, and on Android and iOS it no longer needs a coroutine or await. You can also pass the first set when you start, in BubblOptions.segments.BEFORE · 4.x
lifecycleScope.launch {
BubblSdk.updateSegments(listOf("vip", "london"))
}try await BubblClient.shared.updateSegments(["vip", "london"])await BubblSdk.instance.updateSegments(['vip', 'london']);await Bubbl.updateSegments(['vip', 'london']);AFTER · 5.x
Bubbl.setSegments(listOf("vip", "london"))Bubbl.setSegments(["vip", "london"])await Bubbl.setSegments(['vip', 'london']);Bubbl.setSegments(['vip', 'london']);Track custom events by name
Breakingtrack takes an event name and flat properties instead of a BubblTrackEvent. A name uses A–Z a–z 0–9 . _ : - and is up to 100 characters long, with up to 50 properties whose values are text, numbers, true/false or null. You no longer send location or notification ids with an event: Bubbl reports notification and geofence activity itself.BEFORE · 4.x
lifecycleScope.launch {
BubblSdk.track(BubblTrackEvent(type = "activity", activity = "checkout_started"))
}try await BubblClient.shared.track(BubblTrackEvent(type: "activity", activity: "checkout_started"))await BubblSdk.instance.track(
const BubblTrackEvent(type: 'activity', activity: 'checkout_started'),
);await Bubbl.track({ type: 'activity', activity: 'checkout_started' });AFTER · 5.x
Bubbl.track("checkout_started", mapOf("basket_value" to 42.5))Bubbl.track("checkout_started", properties: ["basket_value": 42.5])await Bubbl.track('checkout_started', {'basket_value': 42.5});Bubbl.track('checkout_started', { basket_value: 42.5 });Draw notifications yourself with a listenerIn 4.x you switched the bundled modal off with
BreakingsetDefaultNotificationModalEnabled(false), enableDefaultNotificationModal or notificationRenderingMode, reported interactions with handleNotificationOpen, handleNotificationCta and the like, and opened a stored notification with openNotificationModal. In 5.x, setNotificationListener gets each notification before Bubbl shows it: return true to show it yourself, then report what happens with reportDisplayed, reportOpened, reportCtaClicked, reportDismissed, reportMediaViewed, reportMediaCompleted and reportSurveyStarted, or openCta to follow its call to action. Return false to let Bubbl show it, or call present(message) later to show it in Bubbl's screen. On Flutter and React Native the listener is asked only while the app is in front; otherwise Bubbl shows the notification itself.BEFORE · 4.x
lifecycleScope.launch {
BubblSdk.setDefaultNotificationModalEnabled(false)
BubblSdk.events.collect { event ->
if (event is BubblEvent.NotificationReceived) {
showMyCard(event.payload)
BubblSdk.handleNotificationOpen(event.payload)
}
}
}
// Bubbl's modal for a stored notification:
// BubblSdk.openNotificationModal(context, payload)try await BubblClient.shared.setDefaultNotificationModalEnabled(false)
for await event in await BubblClient.shared.events {
if case let .notificationReceived(payload) = event {
showMyCard(payload)
try await BubblClient.shared.handleNotificationOpen(payload)
}
}
// Bubbl's modal for a stored notification:
// _ = try await BubblClient.shared.openNotificationModal(payload)await BubblSdk.instance.disableDefaultNotificationModal();
BubblSdk.instance.events.listen((event) {
if (event is BubblNotificationReceivedEvent) {
showMyCard(event.payload);
BubblSdk.instance.handleNotificationOpen(event.payload);
}
});
// Bubbl's modal for a stored notification:
// await BubblSdk.instance.openNotificationModal(payload);await Bubbl.boot({ apiKey: '…', enableDefaultNotificationModal: false });
Bubbl.events.addListener((event) => {
if (event.type === 'notificationReceived') {
showMyBanner(event.payload);
Bubbl.handleNotificationOpen(event.payload);
}
});
// Bubbl's modal for a stored notification:
// await Bubbl.openNotificationModal(payload);AFTER · 5.x
Bubbl.setNotificationListener { message ->
showMyCard(message)
Bubbl.reportDisplayed(message)
true // false: Bubbl shows it
}
// Bubbl's screen for a message you held back:
Bubbl.present(message)Bubbl.setNotificationListener { message in
showMyCard(message)
Bubbl.reportDisplayed(message)
return true // false: Bubbl shows it
}
// Bubbl's screen for a message you held back:
Bubbl.present(message)await Bubbl.setNotificationListener((message) async {
showMyCard(message);
await Bubbl.reportDisplayed(message);
return true; // false: Bubbl shows it
});
// Bubbl's screen for a message you held back:
await Bubbl.present(message);Bubbl.setNotificationListener((message) => {
showMyBanner(message);
Bubbl.reportDisplayed(message);
return true; // false: Bubbl shows it
});
// Bubbl's screen for a message you held back:
Bubbl.present(message);Restyle Bubbl's screen with your app's colours
RemovedBubblNotificationModalStyle, setDefaultNotificationModalStyle and resetDefaultNotificationModalStyle are gone. Bubbl's notification screen takes its look from your app instead, with light and dark variants: Android colour resources, or colour sets in your iOS asset catalogue, named bubbl_scrim, bubbl_surface, bubbl_on_surface, bubbl_on_surface_muted, bubbl_accent, bubbl_on_accent, bubbl_outline, bubbl_media_surface and bubbl_error. On Android you can also override the dimens bubbl_card_radius, bubbl_card_max_width, bubbl_card_padding and bubbl_media_max_height, and every text Bubbl shows is a bubbl_ string resource; on iOS, reword or translate the text with a Bubbl.strings table. Flutter and React Native apps add the same resources in their android and ios projects.BEFORE · 4.x
lifecycleScope.launch {
BubblSdk.setDefaultNotificationModalStyle(
BubblNotificationModalStyle(theme = BubblNotificationModalTheme.Dark, accentColor = "#0F766E")
)
}try await BubblClient.shared.setDefaultNotificationModalStyle(
BubblNotificationModalStyle(theme: .dark, accentColor: "#0F766E")
)await BubblSdk.instance.setDefaultNotificationModalStyle(
const BubblNotificationModalStyle(
theme: BubblNotificationModalTheme.dark,
accentColor: '#0F766E',
),
);await Bubbl.setDefaultNotificationModalStyle({ theme: 'dark', accentColor: '#0F766E' });AFTER · 5.x
<!-- res/values/colors.xml, and res/values-night/colors.xml for dark mode -->
<resources>
<color name="bubbl_accent">#0F766E</color>
</resources>Assets.xcassets › + › Color Set, named bubbl_accent.
Set Appearances to "Any, Dark" and choose a colour for each.<!-- android/app/src/main/res/values/colors.xml (values-night for dark mode).
On iOS, add a bubbl_accent colour set to ios/Runner/Assets.xcassets. -->
<resources>
<color name="bubbl_accent">#0F766E</color>
</resources><!-- android/app/src/main/res/values/colors.xml (values-night for dark mode).
On iOS, add a bubbl_accent colour set to your app's asset catalogue. -->
<resources>
<color name="bubbl_accent">#0F766E</color>
</resources>Remove correlation IDs
RemovedsetCorrelationId, clearCorrelationId and BubblConfig.correlationId have no 5.x equivalent, because 5.x has no field for one. To find a particular device, use its install ID from diagnostics(): search for it under Active users in the dashboard.BEFORE · 4.x
lifecycleScope.launch {
BubblSdk.setCorrelationId("user-123")
}try await BubblClient.shared.setCorrelationId("user-123")await BubblSdk.instance.setCorrelationId('user-123');await Bubbl.setCorrelationId('user-123');AFTER · 5.x
// No equivalent. This install's ID, for support:
lifecycleScope.launch {
val installId = Bubbl.diagnostics().installId
}// No equivalent. This install's ID, for support:
let installId = await Bubbl.diagnostics().installId// No equivalent. This install's ID, for support:
final installId = (await Bubbl.diagnostics()).installId;// No equivalent. This install's ID, for support:
const { installId } = await Bubbl.diagnostics();Submit survey answers with submitSurvey
RenamedsubmitSurveyResponse(BubblSurveyResponse(…)) becomes submitSurvey(message, answers), with the answers keyed by question id. You need it only when you draw surveys yourself, because Bubbl's own screen submits its surveys. The answers are checked as the server checks them, and submitSurvey returns false, logging why, when they can't be sent (a required question left unanswered, say). By question type: single choice takes a choice id, multiple choice a list of choice ids, rating a whole number from 1 to 5, boolean true or false, number a number, slider 0 to 10, and open-ended text up to 2,000 characters.BEFORE · 4.x
lifecycleScope.launch {
val question = payload.survey?.questions?.firstOrNull() ?: return@launch
BubblSdk.submitSurveyResponse(
BubblSurveyResponse(
curatedNotificationId = payload.curatedNotificationId ?: return@launch,
answers = listOf(
BubblSurveyAnswer(questionId = question.id, type = question.type, choiceIds = listOf(choiceId))
)
)
)
}guard let question = payload.survey?.questions.first,
let notificationId = payload.curatedNotificationId else { return }
try await BubblClient.shared.submitSurveyResponse(
BubblSurveyResponse(
curatedNotificationId: notificationId,
answers: [BubblSurveyAnswer(questionId: question.id, type: question.type, choiceIds: [choiceId])]
)
)final question = payload.survey!.questions.first;
await BubblSdk.instance.submitSurveyResponse(
BubblSurveyResponse(
curatedNotificationId: payload.curatedNotificationId!,
answers: [
BubblSurveyAnswer(questionId: question.id, type: question.type, choiceIds: [choiceId]),
],
),
);const question = payload.survey?.questions?.[0];
if (!question || !payload.curatedNotificationId) return;
await Bubbl.submitSurveyResponse({
curatedNotificationId: payload.curatedNotificationId,
answers: [{ questionId: question.id, type: question.type, choiceIds: [choiceId] }],
});AFTER · 5.x
val question = message.questions.first()
val sent = Bubbl.submitSurvey(message, mapOf(question.id to choiceId))guard let question = message.questions.first else { return }
let sent = Bubbl.submitSurvey(message, answers: [question.id: choiceId])final question = message.questions.first;
final sent = await Bubbl.submitSurvey(message, {question.id: choiceId});const question = message.questions[0];
const sent = await Bubbl.submitSurvey(message, { [question.id]: choiceId });Listen to events with addEventListenerEvents now come through
BreakingaddEventListener on every platform (on Flutter, the Bubbl.events stream too), and carry a BubblMessage or a location id instead of 4.x's notification payloads and geofence transitions. Several names change: notification tapped is now notification opened, and CTA tapped is CTA clicked, and there are new events for dismissals, media, surveys and a rejected device credential. Ready, Diagnostic, LocationUpdated, GeofenceSnapshot and NotificationSurveyRequested are gone, and the error event carries only a message. New event types arrive in minor releases, so always give your when or switch a default branch.BEFORE · 4.x
lifecycleScope.launch {
BubblSdk.events.collect { event ->
when (event) {
is BubblEvent.GeofenceEntered -> println(event.transition.locationId)
is BubblEvent.NotificationTapped -> println(event.payload.id)
else -> Unit
}
}
}Task {
for await event in await BubblClient.shared.events {
switch event {
case .geofenceEntered(let transition): print(transition.locationId ?? "")
case .notificationTapped(let payload, _): print(payload.id)
default: break
}
}
}BubblSdk.instance.events.listen((event) {
if (event is BubblGeofenceEnteredEvent) print(event.transition.locationId);
if (event is BubblNotificationTappedEvent) print(event.payload.id);
});const subscription = Bubbl.events.addListener((event) => {
if (event.type === 'geofenceEntered') console.log(event.transition.locationId);
if (event.type === 'notificationTapped') console.log(event.payload.id);
});AFTER · 5.x
val listener = BubblEventListener { event ->
when (event) {
is BubblEvent.GeofenceEntered -> println(event.locationId)
is BubblEvent.NotificationOpened -> println(event.message.id)
else -> Unit // new event types arrive in minor releases
}
}
Bubbl.addEventListener(listener)
// later: Bubbl.removeEventListener(listener)let token = Bubbl.addEventListener { event in
switch event {
case .geofenceEntered(let locationId): print(locationId)
case .notificationOpened(let message): print(message.id)
default: break // new event types arrive in minor releases
}
}
// later: Bubbl.removeEventListener(token)final subscription = Bubbl.addEventListener((event) {
switch (event) {
case BubblGeofenceEntered(:final locationId):
print(locationId);
case BubblNotificationOpened(:final message):
print(message.id);
default:
break; // new event types arrive in minor releases
}
});
// later: await Bubbl.removeEventListener(subscription);const subscription = Bubbl.addEventListener((event) => {
switch (event.type) {
case 'geofence.entered':
console.log(event.locationId);
break;
case 'notification.opened':
console.log(event.message.id);
break;
default:
break; // new event types arrive in minor releases
}
});
// later: subscription.remove();Ask for consent, and honour opt-out and deletionConsent, opt-out and deletion are explicit calls in 5.x; 4.x had none of them. With
NewrequireConsent in BubblOptions, Bubbl does nothing (no network, location or notifications) until you call setConsent(true). setConsent(false) is the same as optOut(), which stops Bubbl for the user and tells the server. deleteMyData() erases the device on the server and on the phone, and Bubbl stays off afterwards.BEFORE · 4.x
// 4.x has no consent, opt-out or deletion calls: an app held back boot until the user agreed.
if (userAgreed) {
appScope.launch { BubblSdk.boot(BubblConfig(apiKey = "…")) }
}// 4.x has no consent, opt-out or deletion calls: an app held back boot until the user agreed.
if userAgreed {
Task { _ = try await BubblClient.shared.boot(BubblConfig(apiKey: "…")) }
}// 4.x has no consent, opt-out or deletion calls: an app held back boot until the user agreed.
if (userAgreed) {
await BubblSdk.instance.boot(const BubblConfig(apiKey: '…'));
}// 4.x has no consent, opt-out or deletion calls: an app held back boot until the user agreed.
if (userAgreed) {
Bubbl.boot({ apiKey: '…' });
}AFTER · 5.x
Bubbl.start(this, "pk_test_…", BubblOptions(baseUrl = "https://api.sandbox.bubbl.tech", requireConsent = true))
// later, when the user agrees:
Bubbl.setConsent(true)
// when the user opts out, or asks you to erase their data:
Bubbl.optOut()
Bubbl.deleteMyData()Bubbl.start(apiKey: "pk_test_…", options: BubblOptions(baseUrl: "https://api.sandbox.bubbl.tech", requireConsent: true))
// later, when the user agrees:
Bubbl.setConsent(true)
// when the user opts out, or asks you to erase their data:
Bubbl.optOut()
Bubbl.deleteMyData()await Bubbl.start(
apiKey: 'pk_test_…',
options: const BubblOptions(baseUrl: 'https://api.sandbox.bubbl.tech', requireConsent: true),
);
// later, when the user agrees:
await Bubbl.setConsent(true);
// when the user opts out, or asks you to erase their data:
await Bubbl.optOut();
await Bubbl.deleteMyData();Bubbl.start('pk_test_…', { baseUrl: 'https://api.sandbox.bubbl.tech', requireConsent: true });
// later, when the user agrees:
Bubbl.setConsent(true);
// when the user opts out, or asks you to erase their data:
Bubbl.optOut();
Bubbl.deleteMyData();Ask for permissions through Bubbl.permissions
NewBubbl.permissions asks for what Bubbl needs, from anywhere in your app. requestNotifications() and requestLocation(always) show Bubbl's privacy view first when the dashboard's privacy notice calls for it, then the system prompt; with always, requestLocation also takes the user through the second step to background location. status() tells you where the device stands (null before start), and openSettings() opens the app's page in system settings, even before start. In 4.x the app asked for permissions itself. You can keep your own flow: Bubbl uses whatever the user allowed.BEFORE · 4.x
// 4.x: the app asked Android itself.
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.POST_NOTIFICATIONS),
REQUEST_CODE
)// 4.x: the app asked iOS itself, or through BubblLocationMonitor.
let monitor = BubblLocationMonitor()
monitor.requestAlwaysAuthorization()
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }// 4.x: the Bubbl plugin had no permission calls. Apps asked through their own code
// or another plugin.// 4.x: the Bubbl package had no permission calls. Apps asked through their own code
// or another library.AFTER · 5.x
lifecycleScope.launch {
Bubbl.permissions.requestNotifications()
val status = Bubbl.permissions.requestLocation(always = true)
}let notifications = await Bubbl.permissions.requestNotifications()
let location = await Bubbl.permissions.requestLocation(always: true)await Bubbl.permissions.requestNotifications();
await Bubbl.permissions.requestLocation(always: true);await Bubbl.permissions.requestNotifications();
await Bubbl.permissions.requestLocation({ always: true });Use Sandbox and Production keysEvery Bubbl company now has two workspaces, each with its own SDK API key (under Configuration › Developers in the dashboard) and base URL. Sandbox, with
Newpk_test_… keys and https://api.sandbox.bubbl.tech, is free and for development and testing. Production, with pk_live_… keys and https://api.bubbl.tech, is for real users. The key and the base URL must match, or the server answers wrong_environment and Bubbl stops. A Sandbox install stays pending, with no geofences, pushes or messages, until you approve it as a test device: Bubbl logs a code at start that you approve under Configuration › Test devices, or the app calls registerTestDevice(code) with a registration code from the dashboard. To go live, swap both the key and the base URL; nothing else changes.BEFORE · 4.x
BubblConfig(apiKey = "…", environment = BubblEnvironment.Staging) // testing
BubblConfig(apiKey = "…", environment = BubblEnvironment.Production) // liveBubblConfig(apiKey: "…", environment: .staging) // testing
BubblConfig(apiKey: "…", environment: .production) // liveconst BubblConfig(apiKey: '…', environment: BubblEnvironment.staging); // testing
const BubblConfig(apiKey: '…', environment: BubblEnvironment.production); // liveBubbl.boot({ apiKey: '…', environment: 'staging' }); // testing
Bubbl.boot({ apiKey: '…', environment: 'production' }); // liveAFTER · 5.x
// Sandbox
Bubbl.start(this, "pk_test_…", BubblOptions(baseUrl = "https://api.sandbox.bubbl.tech"))
// Production
Bubbl.start(this, "pk_live_…", BubblOptions(baseUrl = "https://api.bubbl.tech"))// Sandbox
Bubbl.start(apiKey: "pk_test_…", options: BubblOptions(baseUrl: "https://api.sandbox.bubbl.tech"))
// Production
Bubbl.start(apiKey: "pk_live_…", options: BubblOptions(baseUrl: "https://api.bubbl.tech"))// Sandbox
await Bubbl.start(apiKey: 'pk_test_…', options: const BubblOptions(baseUrl: 'https://api.sandbox.bubbl.tech'));
// Production
await Bubbl.start(apiKey: 'pk_live_…', options: const BubblOptions(baseUrl: 'https://api.bubbl.tech'));// Sandbox
Bubbl.start('pk_test_…', { baseUrl: 'https://api.sandbox.bubbl.tech' });
// Production
Bubbl.start('pk_live_…', { baseUrl: 'https://api.bubbl.tech' });Check the minimum OS versions4.x needed Android
BreakingminSdk 27 and iOS 15. 5.x installs down to Android minSdk 23 and iOS 13.0, so you don't have to raise your app's minimums, but it only runs on Android 8.1 (API 27) or later and iOS 17 or later. Below that, start logs one line and does nothing else: no registration, network calls, permission prompts, location or background work, every other call is a safe no-op, and listeners never fire. On iOS, that means devices on iOS 15 and 16, which 4.x served, no longer get Bubbl. Check Bubbl.isSupported before showing Bubbl-related settings. The iOS package needs Xcode 16 or later.BEFORE · 4.x
// app/build.gradle.kts
android {
defaultConfig {
minSdk = 27 // 4.x's minimum
}
}# Podfile
platform :ios, '15.0' # 4.x's minimum// android/app/build.gradle.kts (and iOS 15.0 in ios/Podfile)
android {
defaultConfig {
minSdk = 27 // 4.x's minimum
}
}// android/build.gradle (and iOS 15.0 for the app target)
buildscript {
ext {
minSdkVersion = 27 // 4.x's minimum
}
}AFTER · 5.x
// minSdk 23 or higher installs; Bubbl runs on API 27 and later.
if (!Bubbl.isSupported) {
hideBubblSettings()
}// iOS 13.0 or later installs; Bubbl runs on iOS 17 and later.
if !Bubbl.isSupported {
hideBubblSettings()
}// minSdk 23 / iOS 13.0 or higher installs; Bubbl runs on Android 8.1+ and iOS 17+.
if (!await Bubbl.isSupported) {
hideBubblSettings();
}// minSdkVersion 23 / iOS 13.0 or higher installs; Bubbl runs on Android 8.1+ and iOS 17+.
if (!Bubbl.isSupported) {
hideBubblSettings();
}Declare background location in the Play Console5.x's manifest declares
BreakingACCESS_BACKGROUND_LOCATION, so every Android app using Bubbl fills in the Location permissions form in the Play Console, with a short video of the feature. requestLocation(always = true) shows the prominent disclosure Play requires (Bubbl's privacy view) before the system prompt when the dashboard's privacy notice is on. If you set the notice to never, or ask for background location yourself, show your own disclosure first. 4.x's foreground location service, and the FOREGROUND_SERVICE_LOCATION permission that came with it, are gone. Without "Allow all the time", geofences still work, but only while the app is open.BEFORE · 4.x
<!-- Merged in from the 4.x SDK's manifest -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<service
android:name="tech.bubbl.sdk.BubblLocationUpdatesService"
android:foregroundServiceType="location" />// 4.x: the app showed its own disclosure and asked for background location itself.// 4.x: the app showed its own disclosure and asked for background location itself.No code change on this platform.
AFTER · 5.x
// Fill in the Play Console's Location permissions form, then ask through Bubbl,
// which shows the prominent disclosure first when the dashboard's privacy notice is on:
lifecycleScope.launch {
Bubbl.permissions.requestLocation(always = true)
}// Fill in the Play Console's Location permissions form, then:
await Bubbl.permissions.requestLocation(always: true);// Fill in the Play Console's Location permissions form, then:
await Bubbl.permissions.requestLocation({ always: true });No code change on this platform.
Leave google-services.json out if you like4.x needed
Newgoogle-services.json and the Google Services Gradle plugin in the app, and reported firebase_config_missing without them. In 5.x, upload your Firebase project's service account under Configuration › Firebase in the dashboard, then either keep your own google-services.json and the plugin (if the app already uses Firebase), or upload the project's google-services.json under Configuration › Firebase › Android apps and leave both out of the app: Bubbl then starts Firebase for you. A google-services.json in the app must come from the same Firebase project as the service account; otherwise the phone gets a token but none of Bubbl's pushes, and nothing reports an error.BEFORE · 4.x
// app/build.gradle.kts, with app/google-services.json in the project
plugins {
id("com.android.application")
id("com.google.gms.google-services")
}// android/app/build.gradle.kts, with android/app/google-services.json in the project
plugins {
id("com.android.application")
id("com.google.gms.google-services")
}// android/app/build.gradle, with android/app/google-services.json in the project
apply plugin: "com.android.application"
apply plugin: "com.google.gms.google-services"No code change on this platform.
AFTER · 5.x
// app/build.gradle.kts, with google-services.json uploaded to the dashboard instead
plugins {
id("com.android.application")
}// android/app/build.gradle.kts, with google-services.json uploaded to the dashboard instead
plugins {
id("com.android.application")
}// android/app/build.gradle, with google-services.json uploaded to the dashboard instead
apply plugin: "com.android.application"No code change on this platform.
Bubbl 5.x is a clean break from 4.x: nothing from the 4.x API keeps its old name, and there's no compatibility layer. Work through the changes below for your platform, then check the result on a device with Bubbl.diagnostics().
No 4.x data carries over5.x works only with fresh installs. On first launch it deletes what a 4.x install of the same app left behind (its queued events, credentials and saved state) and registers as a brand-new device, so every install counts as new.
SDK 5 runs on Android 8.1 (API 27) or later and iOS 17 or later. It still installs on lower versions (Android minSdk 23, iOS 13.0): there, Bubbl.start logs one line and does nothing else, and every other call is a safe no-op.
Base URLs before the 5.0 releaseUntil 5.0 is released, use
https://api.sandbox.staging.bubbl.tech in place of https://api.sandbox.bubbl.tech, and https://api.staging.bubbl.tech in place of https://api.bubbl.tech.