Setting up push notifications
Apple and Google are configured separately, one channel each. A channel holds one provider's credentials, so reaching a user on both platforms means creating two — an Apple channel for their iPhone and a Google channel for their Android device.
Apple iOS (APNs)
Everything below comes from developer.apple.com/account, and needs a paid Apple Developer Program membership.
1Find your Team ID
A1B2C3D4E5.2Create an APNs auth key
.p8 file downloads once. Apple will not let you download it again — if you lose it, the key has to be revoked and a new one created, and a team may only hold two APNs keys at a time. Save it somewhere durable before leaving the page..p12 push certificate, which dies after a year and takes production with it, a .p8 stays valid until somebody revokes it. That is why the channel re-checks the key with Apple periodically rather than watching a date.3Find your Bundle ID
com.example.app. Apple calls this the notification topic, which is the name the channel form uses it under.4Enable Push Notifications in Xcode
aps-environment entitlement. Without the entitlement the app cannot obtain a device token at all, no matter how the channel is configured. Silent pushes additionally need Background Modes → Remote notifications.5Sandbox and production
| Build | Token type | Apple host |
|---|---|---|
| Run from Xcode (development signing) | sandbox | api.sandbox.push.apple.com |
| TestFlight | production | api.push.apple.com |
| App Store | production | api.push.apple.com |
- Build
- Run from Xcode (development signing)
- Token type
- sandbox
- Apple host
api.sandbox.push.apple.com- Build
- TestFlight
- Token type
- production
- Apple host
api.push.apple.com- Build
- App Store
- Token type
- production
- Apple host
api.push.apple.com
BadDeviceToken, which is indistinguishable from an uninstalled app, so a send retired the token. A channel on the wrong environment drained your registered devices quietly rather than failing loudly. A token is now only retired after both hosts have refused it.6Create the channel
.p8 file. Press Verify afterwards: that asks Apple whether the credentials are genuinely accepted, and needs no device to do it.When Apple rejects something
- Two causes. A token from an iOS Simulator always fails this way — a simulator registers locally, Apple never sees the token, and no channel setting will reach one, so a real device is the only way to test a send. Otherwise it is an environment mismatch: check step 5, which is far more often the answer than a bad key. A token much longer than 64 characters is a strong sign you have a simulator’s.
- The Key ID, the Team ID and the .p8 must all belong together. A Key ID from one key paired with another key’s file fails exactly like this. Also check the key has APNs enabled and has not been revoked.
- The channel has no Bundle ID set.
- The key is valid but not permitted for this Bundle ID — check for a typo, and that the App ID belongs to the same team as the key.
- The signed token was outside Apple’s one-hour window, which points at a clock problem on our side rather than your key. Retrying usually clears it.
- The app is no longer installed on that device. Expected over time; real sends retire such tokens automatically.
Android (FCM)
From the Firebase console. Free — there is no paid tier to reach for.
1Open or create a Firebase project
2Generate a service account key
project_id, client_email and private_key — the three values the channel signs with.private_key is stored on a single line with literal \n escapes; reading the file as JSON turns those into real newlines, and copying by hand does not — producing a key that looks perfectly correct on screen and cannot be decoded.3Set the app up
google-services.json from Project settings → Your apps — a different file from the service account, and not the one to upload here. It also needs the Firebase Messaging SDK, and on Android 13 and later the POST_NOTIFICATIONS runtime permission.4Create the channel
.p8 to Google. We do not do that — Apple is configured as its own channel and we talk to Apple directly, which is one hop fewer and one fewer place your key is stored.When Firebase rejects something
- The device token was issued by a different Firebase project. Check the app ships the google-services.json from the same project as this service account.
- The app is no longer installed, or the token has been replaced. Expected over time.
- Firebase could not authenticate onward to APNs. This channel sends to Android; configure iOS as a separate Apple channel.
- The service account was rejected — it may have been disabled or its key deleted. Generate a new private key and replace the credentials.
In your app
A channel can only reach a device that has told Treebars its push token. Three things, on every platform: ask for permission, hand over the token, and report a tap. Nothing here is needed to start the SDK — init is the same call as in the quickstart.
iOS
Ask once you have told the person why — not at launch. The answer is recorded for you. Then register with Apple, and pass the token on when Apple calls back.
import TreebarsSDK
import UserNotifications
// 1. Ask, then register with Apple.
if await Treebars.requestPushPermission() {
await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
}
// 2. Hand over the token Apple gives you.
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Treebars.registerPushToken(deviceToken)
}
// 3. Report a tap, a button press or a swipe-away, and perform the push's action.
// Set UNUserNotificationCenter.current().delegate = self at launch.
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
Treebars.handleNotificationResponse(response)
}
// 4. A silent push: from 0.7.0, how an in-app test shows within seconds on an open app.
// Turn on Background Modes › Remote notifications for the target.
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
Treebars.handleRemoteNotification(userInfo: userInfo) ? .newData : .noData
}requestPushPermission(provisional: true) asks for Apple's quiet trial instead: delivered to Notification Centre with no prompt. A push whose action is “open a screen” is handed to your own router:
Treebars.setPushActionHandler { type, values in
if type == "navigate" { router.open(values["screen"]) }
}Images, GIFs and buttons need a Notification Service Extension, because iOS fetches nothing for a notification by itself. Add the extension target in Xcode and let the SDK fill it in:
import TreebarsSDK
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
Task { contentHandler(await TreebarsNotificationService.enrich(request)) }
}
}Android
The SDK takes no Firebase dependency, so your app keeps its own FirebaseMessagingService and passes two things across: the token, and each message's data.
// 1. Ask (Android 13 and later), from an Activity, once you have said why.
TreebarsPush.requestPermission(activity)
// 2. Hand over the token: at launch, and whenever Firebase issues a new one.
FirebaseMessaging.getInstance().token.addOnSuccessListener { Treebars.registerPushToken(it) }
class AppMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) = Treebars.registerPushToken(token)
// A push with an image, buttons or a carousel arrives as data for the SDK to draw.
// It answers false for any other message, which is then yours as before.
override fun onMessageReceived(message: RemoteMessage) {
if (TreebarsPush.handle(this, message.data)) return
// your own handling
}
}
// 3. Report a tap, in the Activity it opens. Safe on every launch: it does nothing
// for an intent that carries no Treebars push.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent.extras?.let { extras ->
Treebars.trackNotificationOpened(extras.keySet().associateWith { extras.getString(it).orEmpty() })
}
}A push whose action is “open a screen” comes to your own router through TreebarsPush.setActionListener.
React Native
With React Native Firebase's messaging library, which is where the tokens and the messages come from. Each platform registers its own kind of token: Apple's on iOS, Firebase's on Android.
import { getMessaging, onMessage, setBackgroundMessageHandler } from '@react-native-firebase/messaging';
import treebars from '@treebars/react-native-sdk';
// A rich push that arrives while the app is closed. Needs no init().
setBackgroundMessageHandler(getMessaging(), async (message) => {
if (message.data) await treebars.handleRemotePush(message.data);
});
// And while it is open: from 0.7.0 this is how an in-app test shows within seconds.
onMessage(getMessaging(), async (message) => {
if (message.data) await treebars.handleRemotePush(message.data);
});import { Platform } from 'react-native';
import {
getAPNSToken, getInitialNotification, getMessaging, getToken,
onNotificationOpenedApp, onTokenRefresh,
} from '@react-native-firebase/messaging';
const messaging = getMessaging();
// 1. Ask.
await treebars.requestPushPermission();
// 2. Hand over the token.
if (Platform.OS === 'ios') {
const apns = await getAPNSToken(messaging); // null until Apple has answered: try again shortly
if (apns) await treebars.registerPushToken(apns.toLowerCase(), 'apns');
} else {
await treebars.registerPushToken(await getToken(messaging), 'fcm');
onTokenRefresh(messaging, (token) => treebars.registerPushToken(token, 'fcm'));
}
// 3. Report a tap: one that brought the app forward, and one that started it.
onNotificationOpenedApp(messaging, (message) => treebars.trackNotificationOpened(message.data));
getInitialNotification(messaging).then((message) => treebars.trackNotificationOpened(message?.data));On iOS the answer to requestPushPermission is the person's. On Android it comes back at once, before they have answered the system prompt, so do not wait on it to register the token. Images and buttons on iOS need the same Notification Service Extension as a native app, above.
Web
Serve treebars-sw.js from the package at your site's root — or importScripts('/treebars-sw.js') from a service worker you already have — and subscribe from a click, since a browser refuses a prompt nobody asked for. The key is on your web push channel in the dashboard.
button.addEventListener('click', async () => {
await treebars.push.subscribe({ vapidPublicKey: 'B…' });
});
// Or explain first with the SDK's own soft prompt, and ask the browser only after a yes:
treebars.push.prompt({ vapidPublicKey: 'B…', text: 'Get told when your order ships?' });The worker draws the push and reports a click by itself; there is nothing to wire for a tap.
registerPushToken on that device, with the kind of token this channel's provider issues.