Skip to main content

Push Notifications

AFCore integrates with Firebase Cloud Messaging (FCM) and iOS APNs to deliver push notifications.
This guide shows how to configure your app, request permissions, manage tokens, and interact with the unified AFCore.messaging() API.

You’ll need a Firebase project and google-services.json (Android) and APNs/Firebase setup (iOS).


Android — Setup

  1. Firebase Console

    • Create or choose a project → add Android app (package matches your app).
    • Download google-services.json into app/.
    • Enable Cloud Messaging.
  2. Gradle

app/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.gms.google-services") // 👈
}

dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.1.2"))
implementation("com.google.firebase:firebase-messaging")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}
  1. Notification Channel (Android 8+)
Application.onCreate
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"afcore_default",
"AFCore Notifications",
NotificationManager.IMPORTANCE_DEFAULT
)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
  1. SDK Service
    AFCore already declares its own FirebaseMessagingService in the library’s AndroidManifest.xml.
    Do not add another service to your app.

  2. (Optional) Tell AFCore about your desired presentation

AFCore.messaging().setNotificationOptions(
NotificationOptions(androidChannelId = "afcore_default")
)

iOS — Setup

On iOS you must bridge APNs/FCM events to AFCore.

  • Configure Firebase in your AppDelegate with FirebaseApp.configure().
  • Request notification authorization with UNUserNotificationCenter.
  • Register for remote notifications with UIApplication.shared.registerForRemoteNotifications().
  • Forward APNs tokens to Messaging.messaging().apnsToken.
  • Forward FCM tokens and foreground messages into AFCore using AFMessagingHooks (see below).

Permission & token flow (explicit)

ensurePermissionAndRegister(deviceId) is deprecated.
Use AFPermissions to request POST_NOTIFICATIONS (Android 13+) / notification authorization (iOS), then call updateToken(deviceId). updateToken ensures a device token exists and updates it in your backend. getToken() reads the token from your backend, and deleteToken(deviceId) removes the backend association.

Android (Kotlin)

private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

fun preparePush(context: Context) {
val deviceId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)

scope.launch {
// 1) Ask/verify POST_NOTIFICATIONS (Android 13+). requestPermission() returns Unit.
var granted = AFPermissions.isPermissionGranted(Permission.NOTIFICATIONS)
if (!granted) {
AFPermissions.requestPermission(Permission.NOTIFICATIONS) // Unit
granted = AFPermissions.isPermissionGranted(Permission.NOTIFICATIONS) // re-check
}
if (!granted) {
// User denied; consider showing a rationale or deep-link to settings
return@launch
}

// 2) Register or refresh token in your backend (idempotent)
val ok = AFCore.messaging().updateToken(deviceId)
if (!ok) {
// handle failure (retry/backoff)
}
}
}

iOS (Swift)

func preparePush() {
Task {
let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString

// 1) Ask/verify permission via AFPermissions. requestPermission() returns Void.
var granted = (try? await AFPermissions.shared.isPermissionGranted(permission: .notifications)) == true
if !granted {
await AFPermissions.shared.requestPermission(permission: .notifications) // Void
granted = (try? await AFPermissions.shared.isPermissionGranted(permission: .notifications)) == true // re-check
}
guard granted else { return }

// 2) Register or refresh token in your backend (idempotent)
let ok = (try? await AFCore.shared.messaging().updateToken(deviceId: deviceId)) ?? false
if !ok {
// handle failure (retry/backoff)
}
}
}

Token management

Get token (from backend)

val token = AFCore.messaging().getToken()
let token = try await AFCore.shared.messaging().getToken()

Update/refresh token on backend

Useful on first install, after FCM/APNs rotation, or app upgrades.

val ok = AFCore.messaging().updateToken(deviceId)
let ok = try await AFCore.shared.messaging().updateToken(deviceId: deviceId)

Delete token (backend)

Call on logout/unlink to stop notifications for this device.

val ok = AFCore.messaging().deleteToken(deviceId)
let ok = try await AFCore.shared.messaging().deleteToken(deviceId: deviceId)

Topics

Subscribe/unsubscribe to topics for cohort targeting (marketing, beta, region).

AFCore.messaging().subscribe("beta")
AFCore.messaging().unsubscribe("beta")
let ok = try await AFCore.shared.messaging().subscribe(topic: "beta")
let removed = try await AFCore.shared.messaging().unsubscribe(topic: "beta")

Foreground messages

Collect push data while app is active.

private val pushScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

fun observeMessages() {
pushScope.launch {
AFCore.messaging().messages().collect { msg: IncomingMessage ->
// msg.title, msg.body, msg.data (map), msg.topic, etc.
// Show in-app banner or route the user
}
}
}
Task.detached {
for await msg in AFCore.shared.messaging().messages() {
// msg.title, msg.body, msg.data, msg.deeplink, msg.icon
}
}

Cancel the scope/task when appropriate (e.g., onStop).


iOS bridging

Use AFMessagingHooks in AppDelegate to connect APNs/FCM with AFCore:

func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
AFMessagingHooks.setFcmToken(token: fcmToken)
}

Forward foreground messages:

func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
AFMessagingHooks.onForegroundMessage(
title: notification.request.content.title,
body: notification.request.content.body,
deeplink: notification.request.content.userInfo["deeplink"] as? String,
icon: notification.request.content.userInfo["icon"] as? String,
data: notification.request.content.userInfo as? [String: String]
)
completionHandler([.banner, .list, .sound])
}

Best practices

  • Ask early, not immediately: Request notifications at a relevant moment so users understand the value.
  • Idempotent backend: updateToken(deviceId) should be safe to call on every cold start.
  • Handle denial: gracefully continue if permission denied; offer deep‑link to settings.
  • Privacy: document device IDs and tokens in your privacy policy.
  • Environments: QA vs Prod Firebase projects must not share credentials.
  • Logout: call deleteToken(deviceId) when logging out to stop push for that device.
  • Rotation aware: call updateToken if FCM/APNs token changes (AFCore emits hooks; also consider calling on app start).

Troubleshooting

  • No notifications: verify Firebase project, google-services.json, APNs certificates, and bundle IDs.
  • Token is null: Play Services missing/disabled (Android) or APNs not delivered (iOS). Retry later.
  • Background delivery issues: check Doze (Android), OEM restrictions, iOS background modes, and channel importance.
  • Multiple devices: register distinct deviceId values per device.
  • Duplicate service warning (Android): do not add your own FirebaseMessagingService, AFCore provides one.