Introduction
KlugPush adds remote push notifications to AIR apps on iOS, Android and macOS with one extension and one ActionScript API. It asks the user for permission, registers the device, hands the token to ActionScript for your server and delivers incoming notifications to your app.
- iOS and macOS: notifications come directly from Apple's push service (APNs). There is no Firebase SDK in your app. The token is the APNs device token.
- Android: Firebase Cloud Messaging (FCM). Google's messaging library is added automatically when the app is packaged, without any extra extensions. The token is the FCM registration token.
- Tap to open: when the user taps a notification, your app gets its payload, also when the tap started the app, so you can open the right screen directly.
KlugPush is in daily use in our apps on iOS, Android and macOS.
| Extension ID | com.klug.push.Push |
| Current version | 2.0.0 (Push.VERSION) |
| Platforms | iOS (devices), Android (arm, arm64, x86, x86_64), macOS (Apple silicon and Intel). On every other platform, including the iOS Simulator and Windows, the extension loads, isSupported is false and all calls are safe no-ops. |
| ActionScript API | One class: com.klug.push.Push |
| Delivery | com.klug.push.Push.ane (the extension), the ActionScript library (SWC) for code completion, and this documentation |
Requirements
| AIR SDK | 51.x (HARMAN) |
| iOS | iOS 13 or later, real devices |
| Android | Android 6.0 (API 23) or later |
| macOS | macOS 11 or later |
| Apple developer account | iOS and macOS: membership in the Apple Developer Program, an App ID with the Push Notifications capability, and a provisioning profile that contains it |
| Signing | iOS and macOS: the app must be signed with the push notifications entitlement (aps-environment) and the matching provisioning profile |
| Firebase | Android: a Firebase project with your Android app registered, and its google-services.json |
| Server | iOS and macOS: HTTP/2 requests to APNs with a token-based (.p8) key. Android: Firebase Cloud Messaging HTTP v1 |
| Permissions | iOS and Android 13+: requested at runtime with requestAuthorisation(). macOS: requested by register(). Android needs POST_NOTIFICATIONS in the manifest. No Info.plist usage text is needed. |
Add the extension
1 · Copy the ANE and declare it
Copy com.klug.push.Push.ane into your project's extension folder (the folder you pass to ADT with -extdir, or the one your IDE links as native extensions) and add the extension ID to the application descriptor:
<extensions>
<extensionID>com.klug.push.Push</extensionID>
</extensions>
2 · iOS: App ID, entitlement and background mode
- In your Apple developer account, open Certificates, Identifiers & Profiles → Identifiers, select the App ID of your app (its bundle ID is the <id> of your application descriptor), enable Push Notifications and save.
- Create or regenerate the iOS provisioning profiles for this App ID. A profile created before push was enabled does not contain it.
- Add the entitlement and the remote-notification background mode to the <iPhone> part of the application descriptor:
<iPhone>
<InfoAdditions><![CDATA[
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
]]></InfoAdditions>
<Entitlements><![CDATA[
<!-- "development" with a development profile, "production" for App Store, TestFlight and ad hoc -->
<key>aps-environment</key>
<string>development</string>
]]></Entitlements>
</iPhone>
The value must match the profile you sign with. The extension contains no binary for the iOS Simulator; there isSupported is false.
3 · Android: Firebase and manifest additions
- In the Firebase console, add an Android app to your project. Its package name is the package name of your APK, normally air. followed by the <id> of your application descriptor.
- Download google-services.json and put it into the root of the app package, next to your SWF (add it to the files you package with ADT). KlugPush configures Firebase from this file when register() runs.
- firebase-messaging is declared by the extension as a Gradle dependency, so ADT adds it automatically when it packages your Android app; the build machine needs access to Google's Maven repository for that. No extra ANEs are needed.
- Add these entries to the manifest additions. Replace air.YOUR.APP.ID with the package name of your APK:
<android>
<manifestAdditions><![CDATA[
<manifest android:installLocation="auto">
<!-- Android 13 and later: permission to show notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application>
<!-- receives FCM tokens and messages -->
<service android:name="com.klug.push.KlugPushMessagingService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<!-- invisible activity that shows the Android 13 permission dialog -->
<activity android:name="com.klug.push.KlugPushPermissionActivity" android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
<!-- lets the notification channel play a sound file from your app package -->
<provider android:name="com.klug.push.KlugPushFileProvider"
android:authorities="air.YOUR.APP.ID.pushnotificationsfileprovider"
android:exported="false" android:grantUriPermissions="true"/>
</application>
</manifest>
]]></manifestAdditions>
</android>
The provider authority must be exactly your package name followed by .pushnotificationsfileprovider. You need it only if your channel uses your own sound file.
4 · macOS: App ID and entitlements
- Enable Push Notifications for the App ID of your Mac app and create or regenerate the Mac provisioning profile (development profile for testing, distribution profile for the Mac App Store or Developer ID).
- macOS entitlements are applied when you sign the app bundle (for example codesign --entitlements entitlements.plist). Add the push entitlement next to the entitlements your app already uses. TEAMID and com.example.yourapp are placeholders for your team ID and bundle ID:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- push: "development" for development-signed builds, "production" for release builds -->
<key>com.apple.developer.aps-environment</key>
<string>production</string>
<!-- must match the App ID in the embedded provisioning profile -->
<key>com.apple.application-identifier</key>
<string>TEAMID.com.example.yourapp</string>
<key>com.apple.developer.team-identifier</key>
<string>TEAMID</string>
<!-- if the app is sandboxed: outgoing connections, e.g. to send the token to your server -->
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Embed the provisioning profile in the bundle as Contents/embedded.provisionprofile before you sign. On the Mac the entitlement is called com.apple.developer.aps-environment; the iOS key aps-environment has no effect there.
5 · Apple: sandbox or production
APNs has two environments, and an iOS or macOS device token belongs to exactly one of them:
| development | Builds signed with a development certificate and a development profile. Tokens are valid on api.sandbox.push.apple.com. |
| production | App Store, TestFlight, ad hoc and Developer ID builds. Tokens are valid on api.push.apple.com. |
Sending a token to the wrong environment fails with the APNs error BadDeviceToken. Store the environment together with each token on your server if you test both kinds of build. FCM has no such split.
Getting started
The smallest working setup: create the Android channel, ask for permission, register, and send the token to your server. Every call returns at once; the token, errors and notifications arrive later in your callbacks.
import com.klug.push.Push; var push:Push = Push.getInstance(); // singleton: never call new Push() yourself if ( push.isSupported ) // iOS, Android, macOS { // Android: notification channel with a sound file from the app root (created once). // iOS: only the last argument counts (show banners while the app is open). macOS: no effect. push.setup( "alerts", "Alerts", "alert.wav", Push.IMPORTANCE_HIGH, true ); push.requestAuthorisation(); // iOS, Android 13+: system prompt while not decided yet push.register( function( token:String ):void { sendTokenToServer( token ); }, // your app function function( message:String ):void { trace( "push: " + message ); }, function( json:String ):void { handlePush( JSON.parse( json ) ); } // your app function ); }
The token arrives whatever the user answers to the permission prompt: the permission only controls whether notifications are displayed. To open the right screen after a tap, add the few lines from Tap to open the right screen.
Guides
Register and keep the token up to date
Call register() on every app start. The token normally stays the same, but it can change, for example after the app was reinstalled or restored from a backup; Firebase may also replace the token while the app runs, and KlugPush then delivers the new one through the same callback. Compare it with the token you sent last and update your server only when it changed. Calling register() again later is safe; the callbacks you pass replace the earlier ones.
| iOS, macOS | The APNs device token as a lowercase hexadecimal string (64 characters). Send it to APNs with your bundle ID as topic. |
| Android | The FCM registration token (a long string). Send it with FCM HTTP v1. KlugPush keeps it on the device, so the getter token returns it even before register() has completed again. |
The getter token returns the last token, so code that runs later, for example after a sign-in, can read it without registering again. On your server, store the platform with each token, so it knows whether to send through APNs or FCM.
import com.klug.push.Push; import flash.net.SharedObject; private var push:Push = Push.getInstance(); public function initPush():void { if ( !push.isSupported ) return; // other platforms: nothing to do push.setup( "alerts", "Alerts", "alert.wav", Push.IMPORTANCE_HIGH, true ); push.requestAuthorisation(); if ( !push.register( onToken, onPushError, onNotification ) ) trace( "push: register() could not be called" ); } private function onToken( token:String ):void { var so:SharedObject = SharedObject.getLocal( "push" ); if ( so.data.token != token ) { sendTokenToServer( token ); // your app function, e.g. an HTTPS POST so.data.token = token; so.flush(); } } private function onPushError( message:String ):void { // typical causes: missing push entitlement (Apple) or google-services.json (Android) trace( "push registration failed: " + message ); } // later, e.g. after the user signed in to another account public function onSignedIn():void { if ( push.token != null ) sendTokenToServer( push.token ); }
Android: google-services.json, channel and sound
register() configures Firebase from the google-services.json in the root of your app package. When the file is missing, no token arrives and onError receives Firebase is not configured (google-services.json missing). If the file contains several Android apps, KlugPush uses the entry whose package name matches your APK.
On Android 8 and later every notification belongs to a notification channel. setup(channelId, channelName, sound, importance, showWhenActive) creates it:
| channelId | Your ID of the channel. Your server uses the same ID in its messages (android.notification.channel_id). |
| channelName | The name the user sees in the notification settings of your app. |
| sound | A sound file in the root of your app package, for example "alert.wav" (wav, mp3, ogg, m4a). Empty: the default notification sound. |
| importance | Push.IMPORTANCE_HIGH (default, shows a heads-up banner), IMPORTANCE_DEFAULT, IMPORTANCE_LOW or IMPORTANCE_MIN. |
| showWhenActive | Also show notifications while your app is in the foreground (default true). See Notifications while the app is open. |
Call setup() on every start, before register(). It is cheap and makes sure the channel exists before the first notification arrives. On Android 6 and 7 there are no channels; the sound is then used for the notifications KlugPush shows while the app is open.
// first release used "alerts" with alert.wav; the new sound needs a new channel ID push.setup( "alerts_v2", "Alerts", "chime.wav", Push.IMPORTANCE_HIGH, true );
When a message names a channel that does not exist on the device, or names none, Android uses a fallback channel with the default sound. So send channel_id in every message (see Send a test notification).
Tap to open the right screen
When the user taps a notification, consumeSelectedNotification() returns its payload as a JSON string, once; afterwards and when the app was not opened from a notification, it returns an empty string. It works for a cold start (the tap launched the app) and a warm start (the app was in the background). Call it at startup and whenever your app becomes active. While the app is open, iOS also dispatches Push.NOTIFICATION_SELECTED.
import com.klug.push.Push; import flash.desktop.NativeApplication; import flash.events.Event; import flash.events.StatusEvent; private var push:Push = Push.getInstance(); public function initTapToOpen():void { push.addEventListener( StatusEvent.STATUS, onPushStatus ); NativeApplication.nativeApplication.addEventListener( Event.ACTIVATE, onActivate ); openFromPayload( push.consumeSelectedNotification() ); // cold start } private function onActivate( e:Event ):void { openFromPayload( push.consumeSelectedNotification() ); // warm start } private function onPushStatus( e:StatusEvent ):void { // e.level carries the same JSON. Consuming it here as well makes sure that // the ACTIVATE handler does not open the same screen a second time. if ( e.code == Push.NOTIFICATION_SELECTED ) openFromPayload( push.consumeSelectedNotification() ); } private function openFromPayload( json:String ):void { if ( json == null || json == "" ) return; // not opened from a notification var payload:Object; try { payload = JSON.parse( json ); } catch ( err:Error ) { return; } // your own keys are at the top level on iOS and Android if ( payload.hasOwnProperty( "stationId" ) ) openStation( String( payload.stationId ) ); // your app function }
| iOS | The payload is the complete APNs payload: the aps dictionary plus your own keys. Cold start, warm start and taps on banners while the app is open are all covered. |
| Android | The payload contains the keys of the message's data part (always strings) plus a few keys added by Firebase, such as google.message_id. Title and body are not included. A tap brings the app to the foreground, so read it on Event.ACTIVATE; Android dispatches no NOTIFICATION_SELECTED. Data-only messages are not displayed, so they cannot be tapped. |
| macOS | Not available: consumeSelectedNotification() returns an empty string. Load the current state from your server at startup. |
Put your routing information into your own keys (for example stationId) and keep their values short strings. Then the same openFromPayload() works on both platforms.
Notifications while the app is open
onNotification and the Push.NOTIFICATION event receive a notification that arrives while your app is in the foreground, as a JSON string. With showWhenActive (the last argument of setup(), default true) the system banner is shown as well; with false only your code sees it.
| iOS | KlugPush shows the banner with sound and badge while the app is open (showWhenActive true) or suppresses it (false). The JSON is the complete APNs payload. Notifications that arrive while the app is in the background are not forwarded; a tap on them is reported by consumeSelectedNotification(). |
| Android | While the app is in the foreground, Firebase does not display notifications itself. KlugPush then shows them on the channel of the message (or your setup() channel), with your app icon (showWhenActive true). A tap on such a notification is reported by consumeSelectedNotification() like any other. The JSON contains your data keys plus title, body, channel_id and message_id. Data-only messages reach onNotification while the app runs and are never displayed. |
| macOS | onNotification fires when a notification is delivered while the app runs. When the app is in the foreground, macOS normally does not show a banner. setup() has no effect. |
private function onNotification( json:String ):void { var payload:Object; try { payload = JSON.parse( json ); } catch ( e:Error ) { return; } // text: iOS and macOS in aps.alert, Android in title / body var text:String = ""; if ( payload.aps && payload.aps.alert ) text = ( payload.aps.alert is String ) ? String( payload.aps.alert ) : String( payload.aps.alert.body ); else if ( payload.hasOwnProperty( "body" ) ) text = String( payload.body ); // your own keys, e.g. { ..., "stationId": "42" } if ( payload.hasOwnProperty( "stationId" ) ) showInAppHint( String( payload.stationId ), text ); // your app function }
Permission status and notification settings
The system asks only once. After the user declined, the prompt does not appear again; the way back is the notification settings of your app, which openSettings() opens. Who shows the prompt differs per platform:
| iOS | requestAuthorisation() shows the prompt (alerts, sounds, badges) while the user has not decided yet. register() does not ask. |
| Android | Android 13 and later: requestAuthorisation() shows the system dialog for POST_NOTIFICATIONS. Android 6 to 12 has no prompt; the status is authorised unless the user switched notifications off in the settings. |
| macOS | register() shows the prompt the first time. requestAuthorisation() has no effect. |
Calling requestAuthorisation() and register() on every platform is therefore fine. The answer arrives as a Push.AUTHORISATION event whose level is Push.AUTHORISED, Push.DENIED or Push.NOT_DETERMINED. authorisationStatus() returns the same strings, or Push.UNKNOWN while no status is known yet. refreshStatus() reads the live status and also delivers it as an AUTHORISATION event, so use it when your app comes back from the settings:
import com.klug.push.Push; import flash.desktop.NativeApplication; import flash.events.Event; import flash.events.StatusEvent; private var push:Push = Push.getInstance(); public function initSettingsScreen():void { push.addEventListener( StatusEvent.STATUS, onPushStatus ); showStatus( push.authorisationStatus() ); // the user may change the setting in the system settings: read it again on return NativeApplication.nativeApplication.addEventListener( Event.ACTIVATE, onActivate ); push.refreshStatus(); } private function onActivate( e:Event ):void { push.refreshStatus(); // answer arrives as Push.AUTHORISATION } private function onPushStatus( e:StatusEvent ):void { if ( e.code == Push.AUTHORISATION ) showStatus( e.level ); } private function showStatus( status:String ):void { switch ( status ) { case Push.AUTHORISED: setSwitch( "on" ); break; // your app UI case Push.DENIED: setSwitch( "off" ); break; case Push.NOT_DETERMINED: setSwitch( "ask" ); break; } // UNKNOWN: wait for the event } public function onSwitchTapped():void { if ( push.authorisationStatus() == Push.DENIED ) push.openSettings(); // the only way back after a denial else { push.requestAuthorisation(); // iOS, Android 13+ push.register( onToken, onPushError, onNotification ); // macOS shows its prompt here } }
If you prefer numbers, refreshStatus(onStatus) also passes the raw value to onStatus(status:int), and authStatus returns the last raw value without asking the system:
| -1 | Unknown: no status has been delivered yet (Push.UNKNOWN) |
| 0 · STATUS_NOT_DETERMINED | The user has not been asked yet (Push.NOT_DETERMINED) |
| 1 · STATUS_DENIED | Notifications are switched off for the app; on Android 13 and later also after the dialog was dismissed without allowing (Push.DENIED) |
| 2 · STATUS_AUTHORIZED | Notifications are allowed (Push.AUTHORISED) |
| 3, 4 | iOS and macOS: provisional and ephemeral authorisation as reported by the system; reported as Push.AUTHORISED |
The onStatus callback is stored and is called again with every later status, for example the answer to the prompt. Push.statusString(status) converts a raw value into the string constants.
Listen with events instead of callbacks
Every result is also dispatched as a StatusEvent.STATUS on the Push instance. The code names the kind (Push.TOKEN, Push.ERROR, Push.NOTIFICATION, Push.NOTIFICATION_SELECTED, Push.AUTHORISATION, Push.UNREGISTERED), and the level carries the value.
import com.klug.push.Push; import flash.events.StatusEvent; var push:Push = Push.getInstance(); push.addEventListener( StatusEvent.STATUS, onPushStatus ); push.requestAuthorisation(); push.register(); // all callbacks are optional function onPushStatus( e:StatusEvent ):void { switch ( e.code ) { case Push.TOKEN: sendTokenToServer( e.level ); break; // APNs or FCM token case Push.ERROR: trace( "push error: " + e.level ); break; case Push.NOTIFICATION: handlePush( JSON.parse( e.level ) ); break; // JSON payload case Push.AUTHORISATION: trace( "permission: " + e.level ); break; // AUTHORISED, DENIED, ... } }
Send a test notification from your server
Put the information your app needs after a tap into your own keys. In the examples below this is stationId; it arrives at the top level of the payload on iOS and Android.
Your server authenticates with APNs. The token-based way uses one key for all your apps:
- In your Apple developer account, open Certificates, Identifiers & Profiles → Keys, create a key with Apple Push Notifications service (APNs) enabled and download the .p8 file. It can be downloaded only once; keep it secret and never ship it inside the app.
- Note the Key ID of the key and your Team ID (shown in the membership details).
- Create a JSON Web Token with any JWT library: algorithm ES256, signed with the .p8 key, header kid = Key ID, claims iss = Team ID and iat = the current time in seconds. A token stays valid for up to one hour.
Then send an HTTP/2 request. All values in capitals are placeholders:
# development build: api.sandbox.push.apple.com · release build: api.push.apple.com curl --http2 \ -H "authorization: bearer JWT_FROM_YOUR_P8_KEY" \ -H "apns-topic: com.example.yourapp" \ -H "apns-push-type: alert" \ -H "apns-priority: 10" \ -d '{"aps":{"alert":{"title":"New event","body":"Tap to open station 42"},"sound":"default"},"stationId":"42"}' \ https://api.sandbox.push.apple.com/3/device/DEVICE_TOKEN_FROM_ONTOKEN
apns-topic is the bundle ID of your app. A response with status 200 means APNs accepted the notification. On iOS, sound can also name a sound file (wav, aiff or caf, up to 30 seconds) that you package in the root of your app. For a quick test without your own server, the Push Notifications Console in your Apple developer account can also send a notification to a device token.
- In the Firebase console, open Project settings → Service accounts and generate a new private key. Keep the downloaded JSON file on your server only.
- With a Google auth library, get an OAuth 2.0 access token for this service account with the scope https://www.googleapis.com/auth/firebase.messaging.
- POST the message to https://fcm.googleapis.com/v1/projects/PROJECT_ID/messages:send, where PROJECT_ID is the project ID of your Firebase project.
{
"message": {
"token": "FCM_TOKEN_FROM_ONTOKEN",
"notification": {
"title": "New event",
"body": "Tap to open station 42"
},
"data": {
"stationId": "42"
},
"android": {
"priority": "high",
"notification": { "channel_id": "alerts" }
}
}
}
curl -X POST \ -H "Authorization: Bearer ACCESS_TOKEN_OF_YOUR_SERVICE_ACCOUNT" \ -H "Content-Type: application/json" \ -d @fcm-message.json \ https://fcm.googleapis.com/v1/projects/PROJECT_ID/messages:send
The notification part is what the user sees; the data part reaches your app after a tap and in onNotification. All data values must be strings. channel_id must be the channelId you passed to setup(); it decides the sound. A response with status 200 contains the message name. A message without the notification part is a data-only message: it is not displayed and reaches your app only while it runs.
Unregister
unregister() behaves differently per platform. On iOS it unregisters the app from APNs; no notifications arrive until register() runs again. On Android the FCM token stays valid, so stop sending to it on your server (for example when the user signs out). On macOS it does nothing. On iOS and Android a Push.UNREGISTERED event confirms the call.
Cross-platform apps
The extension contains a fallback for every other platform, including the iOS Simulator and Windows. There isSupported is false, register() returns false, consumeSelectedNotification() returns an empty string and the other methods do nothing, so the same code can stay in an app that builds for all platforms. Methods that a platform does not need are safe no-ops there as well, for example setup() and requestAuthorisation() on macOS.
API reference
com.klug.push.Push
Extends flash.events.EventDispatcher. Singleton.
| getInstance() | public static function getInstance():Push Returns the single instance and creates it on the first call. Calling new Push() a second time throws an Error. |
| isSupported | public function get isSupported():Boolean True on iOS devices, Android and macOS, false on all other platforms. It does not check signing, entitlements or Firebase configuration. |
| setup() | public function setup(channelId:String, channelName:String, sound:String = "", importance:int = IMPORTANCE_HIGH, showWhenActive:Boolean = true):void Android: creates the notification channel (only if no channel with this ID exists) with a sound file from the app root and stores showWhenActive. iOS: only showWhenActive is used (show banners while the app is open). macOS: no effect. |
| requestAuthorisation() | public function requestAuthorisation():void iOS and Android 13+: shows the system prompt while the user has not decided yet. The result arrives as an AUTHORISATION event (also when no prompt was needed, on Android). macOS: no effect; register() asks there. |
| authorisationStatus() | public function authorisationStatus():String Returns AUTHORISED, DENIED, NOT_DETERMINED or UNKNOWN. Android reads the live status. iOS returns the status read at startup or by the last requestAuthorisation() or refreshStatus(). macOS returns UNKNOWN until register() or refreshStatus() has delivered a status. Also updates authStatus. |
| register() | public function register(onToken:Function = null, onError:Function = null, onNotification:Function = null):Boolean Registers with APNs (iOS, macOS) or Firebase Cloud Messaging (Android). On macOS it first shows the permission prompt. onToken(token:String) receives the token, onError(message:String) the message if registration fails, onNotification(json:String) the payload of each notification received while the app runs. Callbacks you pass replace those of an earlier call. Returns true if the request was handed to the native side, false where push is not supported. Asynchronous. |
| token | public function get token():String The last token received, or null until registration has delivered one. Android also returns the token stored by an earlier start. |
| consumeSelectedNotification() | public function consumeSelectedNotification():String The JSON payload of the notification the user tapped to open the app (cold or warm start), returned once. Returns an empty string when the app was not opened from a notification, when it was already consumed, and on macOS. Call it at startup and on Event.ACTIVATE. |
| unregister() | public function unregister():void iOS: unregisters from APNs and dispatches UNREGISTERED. Android: keeps the FCM token and dispatches UNREGISTERED. macOS: no-op. |
| authStatus | public function get authStatus():int The last known raw permission status (see the status constants); -1 until a status has been delivered. Does not query the system. |
| refreshStatus() | public function refreshStatus(onStatus:Function = null):void Reads the live permission status. The result arrives as an AUTHORISATION event and, if given, at onStatus(status:int). The callback is kept and also receives every later status. Updates authStatus. |
| canOpenSettings | public function get canOpenSettings():Boolean True where openSettings() works (iOS, Android, macOS). |
| openSettings() | public function openSettings():void Opens the notification settings of your app: the Settings app on iOS, the app's notification settings on Android, System Settings at Notifications on macOS. No-op where not supported. |
| statusString() | public static function statusString(s:int):String Converts a raw status into NOT_DETERMINED, DENIED, AUTHORISED (2, 3, 4) or UNKNOWN. |
| VERSION | public static const VERSION:String = "2.0.0" |
Constants
| TOKEN | "token" · event code: the level is the token |
| ERROR | "error" · event code: the level is the error message |
| NOTIFICATION | "notification" · event code: the level is the payload as JSON, received while the app runs |
| NOTIFICATION_SELECTED | "notificationSelected" · event code: the level is the payload as JSON of the notification the user tapped |
| AUTHORISATION | "authorisation" · event code: the level is the permission status string |
| UNREGISTERED | "unregistered" · event code: confirms unregister() |
| AUTHORISED, DENIED, NOT_DETERMINED, UNKNOWN | "authorised", "denied", "not_determined", "unknown" · permission status strings |
| IMPORTANCE_MIN, _LOW, _DEFAULT, _HIGH | 1, 2, 3, 4 · Android channel importance for setup(), the same values as Android's NotificationManager |
| STATUS_NOT_DETERMINED, STATUS_DENIED, STATUS_AUTHORIZED | 0, 1, 2 · raw permission status for authStatus and refreshStatus() |
Events
All results arrive asynchronously, on the main thread of your app, as flash.events.StatusEvent.STATUS on the Push instance. The code names the kind, the level carries the value. Token, error and notification also call the callbacks of register().
| code "token" | Push.TOKEN · iOS, Android, macOS. Fires after register() when the token is issued, and on Android also when Firebase replaces the token. level = the token. Also calls onToken and updates token. |
| code "error" | Push.ERROR · iOS, Android, macOS. Fires when registration fails, for example without the push entitlement or google-services.json. level = the error message. Also calls onError. |
| code "notification" | Push.NOTIFICATION · iOS, Android, macOS. Fires when a notification arrives while the app is running (iOS: in the foreground). level = the payload as JSON. Also calls onNotification. |
| code "notificationSelected" | Push.NOTIFICATION_SELECTED · iOS. Fires when the user taps a notification while the app is running. level = the payload as JSON. On Android use consumeSelectedNotification() on Event.ACTIVATE. |
| code "authorisation" | Push.AUTHORISATION · iOS, Android, macOS. Fires after requestAuthorisation(), refreshStatus() and, on macOS, the prompt of register(). level = AUTHORISED, DENIED or NOT_DETERMINED. Also calls the stored onStatus callback with the raw value. |
| code "unregistered" | Push.UNREGISTERED · iOS, Android. Fires after unregister(). level is empty. |
Troubleshooting
Android: no token, onError says Firebase is not configured
google-services.json is not in the root of the app package. Add it to the files you package with ADT, at the top level next to your SWF, and check that it belongs to the Firebase project and the package name of your APK (normally air. + your app ID). Also check that the KlugPushMessagingService entry is in the manifest additions.
Android: the notification has the wrong sound or none
The channel already existed on the device with another sound. Android never changes the sound of an existing channel, and setup() leaves it untouched. Use a new channelId and send it as channel_id from your server, or uninstall the app on your test device. Also check that the sound file is in the root of the app package, that the KlugPushFileProvider authority is your package name followed by .pushnotificationsfileprovider, and that your messages name the channel: without channel_id, Android uses a fallback channel with the default sound.
Android 13: no permission dialog, or nothing is displayed
Since Android 13 an app must hold POST_NOTIFICATIONS to show notifications. Declare the permission and the KlugPushPermissionActivity in the manifest additions and call requestAuthorisation(). Without the activity no dialog appears and only the current status is reported. After the user declined twice, Android shows no dialog any more and the status stays DENIED; call openSettings(). The token arrives in any case.
iOS: no token, onError reports a missing aps-environment entitlement
The app is not signed with the aps-environment entitlement, or the provisioning profile does not contain the Push Notifications capability. Enable push for the App ID, regenerate the profile, add the entitlement to <Entitlements> with the value that matches the profile, and package again.
iOS: isSupported is false
The app runs in the iOS Simulator, for which the extension contains no native part. Test push on a real device.
iOS: no banner while the app is open, or taps are not reported
Check that you did not pass false as the last argument of setup(). KlugPush shows foreground banners and reports taps as the delegate of the iOS notification center. If another extension in your app already took that role, KlugPush leaves it in place; use only one push extension.
macOS: onError reports a missing entitlement, or nothing arrives at all
The app bundle is not signed with com.apple.developer.aps-environment, or the embedded provisioning profile does not contain the Push Notifications capability. Enable push for the App ID, regenerate the profile, embed it and sign again. Ad hoc signed local builds cannot receive a token.
The server gets BadDeviceToken
The token and the APNs host belong to different environments. Tokens of development-signed builds work only with api.sandbox.push.apple.com, tokens of release builds only with api.push.apple.com. A token from an Android device must be sent through FCM, not APNs.
The user declined and the prompt never appears again
This is how iOS, Android and macOS work. Check authorisationStatus() or call refreshStatus(); if it is DENIED, explain the situation and call openSettings(). Read the status again when your app becomes active.
authorisationStatus() is UNKNOWN, authStatus is -1
No status has been delivered yet. On macOS that needs register() or refreshStatus(); on iOS the status is read in the background right after startup. Call refreshStatus() and wait for the AUTHORISATION event instead of reading the value right after the call.
My StatusEvent listener stopped working after the update to 2.0.0
Since 2.0.0 the kind is in e.code and the value in e.level; 1.0.0 did it the other way round. Compare e.code with Push.TOKEN, Push.ERROR, Push.NOTIFICATION and the other constants, and read the value from e.level. Callbacks are not affected.
The app opens after a tap, but consumeSelectedNotification() returns ""
The payload is returned only once, so another part of your code may have consumed it already; call it in one place. On Android only displayed notifications can be tapped, so a message needs a notification part. On macOS tap payloads are not available.
isSupported is true, but push does not work in my test build
isSupported only tells you that the native part is present. Signing and Firebase problems show up later, through onError.
Changelog
2.0.0
- iOS and Android: iOS directly through APNs without a Firebase SDK; Android through Firebase Cloud Messaging, added via Gradle at packaging, no extra ANEs
- Tap to open: consumeSelectedNotification() returns the payload of the tapped notification after a cold or warm start; NOTIFICATION_SELECTED event on iOS
- New methods setup() (Android channel, sound, importance, foreground banners), requestAuthorisation(), authorisationStatus(), unregister() and canOpenSettings; new events AUTHORISATION and UNREGISTERED
- Notifications while the app is open are shown as a banner on iOS and Android (showWhenActive)
- Breaking change for event listeners: StatusEvent code now names the kind and level carries the value (the reverse of 1.0.0). Callbacks are unchanged.
1.0.0
- Permission status: authStatus, refreshStatus() and the status constants
- openSettings() opens System Settings at Notifications
- First release (macOS): permission prompt, APNs registration, device token, notifications delivered to ActionScript as JSON, StatusEvents and callbacks
- Universal binary for Apple silicon and Intel Macs; safe fallback on all other platforms