Push

Configurer votre application

Le côté Apple et Google du push : les clés avec lesquelles honkio envoie, le réglage dont votre application a besoin et le code qui transmet le jeton de l'appareil à votre serveur.

Avant de commencer

Pour iOS, il vous faut une adhésion à l'Apple Developer Program, pour créer la clé avec laquelle honkio envoie à votre application. Pour Android, il vous faut un projet Firebase, qui est gratuit. Une fois ces identifiants ajoutés à votre application, vous pouvez développer avec une clé de test : le mode test n'envoie rien à Apple ni à Google, ne facture rien et accepte des jetons magiques, sans téléphone (Mode test).

Votre application ne communique jamais avec honkio. Elle obtient un jeton d'Apple ou de Google et l'envoie à votre propre serveur, qui l'enregistre chez honkio. Gardez votre clé d'API honkio sur votre serveur : une clé intégrée à une application peut en être extraite. Les paquets Swift et Android de HonkIO utilisés plus bas n'appellent jamais honkio et ne contiennent aucune clé d'API.

iOS : Apple

  1. Dans votre compte Apple Developer, ouvrez Certificates, Identifiers & Profiles, puis Identifiers. Ouvrez l'App ID de votre application (un identifiant de bundle explicite, pas un joker) et activez Push Notifications.
  2. Sous Keys, ajoutez une clé en cochant Apple Push Notifications service (APNs) et téléchargez le fichier .p8. Apple ne vous laisse le télécharger qu'une fois : rangez-le en lieu sûr. Notez l'identifiant de clé affiché avec la clé et votre identifiant d'équipe, affiché sous Membership details. Si Apple demande pour quel environnement est la clé, choisissez les deux, pour qu'une seule clé couvre les versions de développement et de diffusion.
  3. Dans Xcode, sélectionnez la cible de votre application, ouvrez Signing & Capabilities et ajoutez Push Notifications. Ajoutez Background Modes en cochant Remote notifications seulement si vous envoyez des push de données seules, et une Notification Service Extension seulement si vous envoyez des notifications avec chiffrement.
  1. Demandez la permission et inscrivez l'appareil auprès d'Apple à chaque lancement, comme d'habitude. Ajoutez le paquet Swift HonkioPush à la cible de votre application (iOS 17 ou plus) et servez-vous-en pour convertir le jeton en texte hexadécimal, le format attendu par honkio. Envoyez ce jeton, son environnement APNs et la clé publique de chiffrement de l'appareil (HonkioPush.encryptionPublicKey) à votre serveur ; l'application sait dans quel environnement elle est (Bac à sable ou production).
swift
.package(url: "https://github.com/somecorp-dev/honkio-swift", from: "0.1.0")

Dans le délégué de l'application :

swift
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) }
}
Sans le paquet

Demandez l'autorisation, inscrivez l'appareil auprès d'Apple à chaque lancement et envoyez le jeton à votre serveur. L'exemple est une application SwiftUI; dans une application UIKit, le délégué d'application est le même.

swift
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)
}

Testez sur un vrai iPhone ou iPad, ou dans le simulateur sur un Mac à puce Apple. Une version lancée depuis Xcode reçoit un jeton du bac à sable : votre serveur l'enregistre donc avec apns_environment sandbox (Bac à sable ou production).

Android : Firebase

  1. Dans la console Firebase, créez un projet (Google Analytics n'est pas nécessaire), puis ajoutez une application Android avec le nom de paquet de votre application. Téléchargez google-services.json dans le module de votre application et suivez les étapes de la console pour ajouter le plugiciel Gradle des services Google et la BoM Firebase.
  2. Ajoutez la bibliothèque de messagerie au module de votre application : implementation("com.google.firebase:firebase-messaging"). Avec la BoM en place, aucune version n'est nécessaire.
  3. Déclarez l'autorisation de notification et le service de messagerie dans AndroidManifest.xml.
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>
  1. Sur Android 13 et plus, demandez d'abord la permission de notification. Ajoutez la bibliothèque honkio-push au module de votre application. Envoyez le jeton courant à votre serveur à chaque lancement avec HonkioTokens.current, et de nouveau depuis onNewToken, avec la clé publique de chiffrement de l'appareil obtenue par HonkioPush.encryptionPublicKey.
kotlin
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")
}
kotlin
// 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))
}
Sans le paquet

Demandez l'autorisation sur Android 13 et plus, lisez le jeton à chaque lancement et chaque fois qu'il change, et envoyez-le à votre serveur.

kotlin
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()
        }
    }
}
  1. Pour honkio, ouvrez les paramètres du projet, puis Comptes de service, et générez une nouvelle clé privée. Le fichier JSON téléchargé est l'identifiant que vous donnez à honkio. Quiconque l'a en main peut envoyer des push à vos utilisateurs et se servir de votre projet Firebase : gardez-le hors de votre application et de votre dépôt de code.

Sur un émulateur, utilisez une image système avec Google Play. Quand votre application est en arrière-plan, Android affiche lui-même un push qui a un titre et un texte. Quand l'application est ouverte, et pour les push de données seules ou envoyés avec chiffrement, c'est onMessageReceived qui reçoit le push, et rien ne s'affiche à moins que votre application publie elle-même la notification; la page Chiffrement montre comment déchiffrer le push d'abord.

Transmettre le jeton à honkio

Votre serveur reçoit le jeton de la personne connectée et l'enregistre avec PUT /v1/push/devices, avec votre propre identifiant de cette personne comme user_id. L'enregistrement crée ou met à jour l'appareil : le faire à chaque lancement ne pose aucun problème, et enregistrer un jeton sous un autre user_id le déplace, comme sur un téléphone partagé. Transmettez aussi la clé publique de l'appareil dans encryption_public_key, pour que honkio chiffre ses notifications (Chiffrement).

// 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)

Appelez DELETE /v1/push/devices/:id quand la personne se déconnecte. Les règles des appareils sont dans Appareils.

Bac à sable ou production (iOS)

Apple a deux environnements push, et un jeton ne fonctionne que dans celui pour lequel sa version a été signée. Ce qui décide, c'est la façon dont la version a été signée, pas le fait qu'elle soit en Debug ou en Release.

Versionapns_environment
Lancée depuis Xcode sur un appareilsandbox
TestFlightproduction
App Storeproduction
Ad hoc ou entrepriseproduction

Le #if DEBUG de l'exemple correspond aux réglages par défaut de Xcode; une version Release lancée depuis Xcode reçoit tout de même un jeton du bac à sable. Apple refuse un jeton enregistré pour le mauvais environnement comme un jeton invalide, et honkio retire l'appareil avec le motif INVALID_TOKEN. L'enregistrer de nouveau avec le bon environnement le rétablit.

Android n'a pas d'environnements : le même jeton fonctionne pour toutes les versions.

Ajouter les identifiants dans honkio

Dans le tableau de bord, passez en mode Production, ouvrez Push, puis Applications, et choisissez Nouvelle application : une application est commune aux modes test et production, et seul le mode Production peut en créer une. Collez le texte de la clé .p8 avec son identifiant de clé, votre identifiant d'équipe et l'identifiant de bundle, le contenu du fichier JSON Firebase, ou les deux. honkio les vérifie auprès d'Apple et de Google avant d'enregistrer l'application. Pour le faire par le code, avec une clé de production, voyez Identifiants.

Envoyez ensuite un push avec une clé de production au user_id que vous avez enregistré (Envoi et repli). Les 2 500 premiers push acceptés par Apple ou Google chaque mois sont gratuits.