Push
Set up your app
The Apple and Google side of push: the keys honkio sends with, the setting your app needs, and the code that gets a device token to your backend.
Before you start
For iOS you need a membership in the Apple Developer Program, to create the key honkio sends to your app with. For Android you need a Firebase project, which is free. Once your app has those credentials, you can build against a test key: test mode sends nothing to Apple or Google, charges nothing and takes magic tokens, so it needs no phone (Test mode).
Your app never talks to honkio. It gets a token from Apple or Google and sends it to your own backend, which registers it with honkio. Keep your honkio API key on your server: a key built into an app can be read out of it. The HonkIO Swift and Android packages used below never call honkio and hold no API keys.
iOS: Apple
- In your Apple Developer account, open Certificates, Identifiers & Profiles, then Identifiers. Open your app's App ID (an explicit bundle ID, not a wildcard) and turn on Push Notifications.
- Under Keys, add a key with Apple Push Notifications service (APNs) ticked and download the .p8 file. Apple lets you download it once, so keep it somewhere safe. Note the key ID shown with the key, and your team ID, shown under Membership details. If Apple asks which environment the key is for, choose both, so one key covers development and release builds.
- In Xcode, select your app target, open Signing & Capabilities and add Push Notifications. Add Background Modes with Remote notifications ticked only if you send data-only pushes, and a Notification Service Extension only if you send encrypted pushes.
- Ask for permission and register with Apple on every launch, as usual. Add the
HonkioPushSwift package to your app target (iOS 17 or later) and use it to turn the token into the hex text honkio expects. Send that token, its APNs environment and the device's encryption public key (HonkioPush.encryptionPublicKey) to your backend; the app knows which environment it has (Sandbox or production).
.package(url: "https://github.com/somecorp-dev/honkio-swift", from: "0.1.0")In the app delegate:
import HonkioPush
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = HonkioDeviceToken.hexString(from: deviceToken)
#if DEBUG
let environment = "sandbox" // a build you run from Xcode
#else
let environment = "production" // TestFlight, App Store and ad hoc builds
#endif
// Created on first use, then the same key on every launch.
guard let key = try? HonkioPush.encryptionPublicKey(accessGroup: "group.com.example.app") else { return }
// sendTokenToYourBackend is your app's own function; your backend registers all three with HonkIO
// (key.publicKey as encryption_public_key).
Task { await sendTokenToYourBackend(token, apnsEnvironment: environment, encryptionPublicKey: key.publicKey) }
}Without the package
Ask for permission, register with Apple on every launch and send the token to your backend. The sample is a SwiftUI app; in a UIKit app the app delegate is the same.
import SwiftUI
import UIKit
import UserNotifications
@main
struct AcmeApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup { Text("Acme") }
}
}
final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
Task {
// Asks once; on later launches it answers straight away with the person's choice.
let granted = (try? await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
// Register on every launch: Apple can change the token.
if granted { application.registerForRemoteNotifications() }
}
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// HonkIO takes the token as hex text. Don't assume a fixed length.
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
Task { await sendTokenToBackend(token) }
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
// Most often the Push Notifications capability is missing from the target.
print("Push registration failed: \(error)")
}
// Show a push that arrives while the app is open, instead of dropping it.
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
[.banner, .sound, .badge]
}
}
/// Your own endpoint, called as the signed-in person. Your backend then calls PUT /v1/push/devices.
func sendTokenToBackend(_ token: String) async {
#if DEBUG
let environment = "sandbox" // a build you run from Xcode
#else
let environment = "production" // TestFlight, App Store and ad hoc builds
#endif
var request = URLRequest(url: URL(string: "https://api.example.com/me/push-token")!)
request.httpMethod = "PUT"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Add your own sign-in header here, so your backend knows whose device this is.
request.httpBody = try? JSONEncoder().encode([
"token": token, "platform": "ios", "apns_environment": environment,
])
_ = try? await URLSession.shared.data(for: request)
}Test on a real iPhone or iPad, or on the Simulator on an Apple silicon Mac. A build you run from Xcode gets a sandbox token, so your backend registers it with apns_environment sandbox (Sandbox or production).
Android: Firebase
- In the Firebase console, create a project (Google Analytics is not needed), then add an Android app with your app's package name. Download
google-services.jsoninto your app module and follow the console's steps to add the Google services Gradle plugin and the Firebase BoM. - Add the messaging library to your app module:
implementation("com.google.firebase:firebase-messaging"). With the BoM in place it needs no version. - Declare the notification permission and the messaging service in
AndroidManifest.xml.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- Android 13 and later ask the person before an app may show notifications. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<!-- Your activities as usual, plus the service that hears about new tokens. -->
<service
android:name=".PushService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>- On Android 13 and later, ask for the notification permission first. Add the
honkio-pushlibrary to your app module. Send the current token to your backend on every launch withHonkioTokens.current, and again fromonNewToken, with the device's encryption public key fromHonkioPush.encryptionPublicKey.
dependencies {
implementation(platform("com.google.firebase:firebase-bom:34.5.0"))
implementation("com.google.firebase:firebase-messaging")
implementation("ca.honkio:honkio-push:0.1.0")
}// sendTokenToBackend is your app's own function. onToken runs on the main thread, so move off it.
HonkioTokens.current(
onToken = { token -> thread { sendTokenToBackend(token, HonkioPush.encryptionPublicKey(applicationContext)) } },
onError = { e -> Log.w("push", "no FCM token", e) },
)
class PushService : FirebaseMessagingService() {
// onNewToken already runs off the main thread.
override fun onNewToken(token: String) = sendTokenToBackend(token, HonkioPush.encryptionPublicKey(applicationContext))
}Without the package
Ask for the permission on Android 13 and later, read the token on every launch and whenever it changes, and send it to your backend.
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import org.json.JSONObject
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread
class MainActivity : ComponentActivity() {
private val askPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
askPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
}
// The current token, on every launch.
FirebaseMessaging.getInstance().token.addOnSuccessListener { token -> sendTokenToBackend(token) }
}
}
class PushService : FirebaseMessagingService() {
// Google issued a new token: the old one stops working.
override fun onNewToken(token: String) = sendTokenToBackend(token)
// While the app is in the background, Android shows a push with a title and body
// itself. This runs for those while the app is open, and for data-only and
// encrypted pushes always. Nothing is shown unless you post the notification
// here: the encryption page's Kotlin sample has a channel and a builder.
override fun onMessageReceived(message: RemoteMessage) {}
}
/** Your own endpoint, called as the signed-in person. Your backend then calls PUT /v1/push/devices. */
fun sendTokenToBackend(token: String) {
thread {
var conn: HttpURLConnection? = null
try {
conn = URL("https://api.example.com/me/push-token").openConnection() as HttpURLConnection
conn.requestMethod = "PUT"
conn.doOutput = true
conn.setRequestProperty("Content-Type", "application/json")
// Add your own sign-in header here, so your backend knows whose device this is.
val body = JSONObject().put("token", token).put("platform", "android").toString()
conn.outputStream.use { it.write(body.toByteArray()) }
conn.responseCode // sends the request and waits for the answer
} catch (e: IOException) {
// Offline or your backend is down: the next launch sends the token again.
} finally {
conn?.disconnect()
}
}
}- For honkio, open Project settings, then Service accounts, and generate a new private key. The JSON file it downloads is the credential you give honkio. Anyone with it can send pushes to your users and use your Firebase project, so keep it out of your app and your code repository.
On an emulator, use a system image with Google Play. While your app is in the background, Android shows a push with a title and body by itself. While the app is open, and for data-only and encrypted pushes, onMessageReceived gets the push instead, and nothing is shown unless your app posts the notification itself; the encryption page shows how to decrypt the push first.
Pass the token to honkio
Your backend receives the token from the signed-in person and registers it with PUT /v1/push/devices, with your own ID for that person as user_id. Registering is an upsert, so doing it on every launch is fine, and registering a token under another user_id moves it, as on a shared phone. Pass the device's public key as encryption_public_key too, so honkio encrypts its pushes (Encryption).
// Call this from your backend when the app reports its token.
const { data: device, error } = await honkio.push.devices.register({
appId: 'APP_ID',
token: 'DEVICE_TOKEN',
platform: 'ios', // or 'android'
userId: 'user_1042', // your own identifier for this person
apnsEnvironment: 'production', // iOS only: 'sandbox' for development builds
encryptionPublicKey: 'DEVICE_PUBLIC_KEY', // the key the HonkIO push package made on the device
})
if (error) throw new Error(error.message)
console.log(device.id, device.active, device.encryption_key_id)from honkio import Honkio
client = Honkio() # reads HONKIO_API_KEY
# Call this from your backend when the app reports its token.
device = client.push.devices.register(
app_id="APP_ID",
token="DEVICE_TOKEN",
platform="ios", # or "android"
user_id="user_1042", # your own identifier for this person
apns_environment="production", # iOS only: "sandbox" for development builds
encryption_public_key="DEVICE_PUBLIC_KEY", # the key the HonkIO push package made on the device
)
print(device["id"], device["active"], device["encryption_key_id"])<?php
use Honkio\Honkio;
$honkio = new Honkio(); // reads HONKIO_API_KEY
// Call this from your backend when the app reports its token.
$device = $honkio->push->devices->register([
'app_id' => 'APP_ID',
'token' => 'DEVICE_TOKEN',
'platform' => 'ios', // or 'android'
'user_id' => 'user_1042', // your own identifier for this person
'apns_environment' => 'production', // iOS only: 'sandbox' for development builds
'encryption_public_key' => 'DEVICE_PUBLIC_KEY', // the key the HonkIO push package made on the device
]);
echo $device['id'], ' ', $device['active'] ? 'active' : 'inactive', ' ', $device['encryption_key_id'] ?? 'no key', PHP_EOL;curl -X PUT https://api.honkio.ca/v1/push/devices \
-H "Authorization: Bearer mk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"app_id": "APP_ID",
"token": "test_accept",
"platform": "ios",
"user_id": "user_1042",
"apns_environment": "sandbox",
"encryption_public_key": "DEVICE_PUBLIC_KEY"
}'
# Response: { "id": "pdev_...", "active": true, "encryption_key_id": "cHRL-UfvREIeEx3Q", ... }Call DELETE /v1/push/devices/:id when the person signs out. The device rules are in Devices.
Sandbox or production (iOS)
Apple has two push environments, and a token only works in the one its build was signed for. What decides it is how the build was signed, not whether it is a Debug or Release build.
| Build | apns_environment |
|---|---|
| Run from Xcode on a device | sandbox |
| TestFlight | production |
| App Store | production |
| Ad hoc or enterprise | production |
The #if DEBUG in the sample matches Xcode's defaults; a Release build you run from Xcode still gets a sandbox token. Apple refuses a token registered for the wrong environment as a bad token, and honkio retires the device with the reason INVALID_TOKEN. Registering it again with the right environment brings it back.
Android has no environments: the same token works for every build.
Add the credentials to honkio
In the dashboard, switch to Live, open Push, then Apps, and choose New app: an app is shared by test and live mode, so only Live can create one. Paste the text of the .p8 key with its key ID, your team ID and the bundle ID, the contents of the Firebase JSON file, or both. honkio checks them with Apple and Google before it saves the app. To do it from code, with a live key, see Credentials.
Then send a push with a live key to the user_id you registered (Sending and fallback). The first 2,500 pushes Apple or Google accept each month are free.
HonkIO