Introduction
KlugPush adds remote push notifications to AIR apps on macOS. It asks the user for permission to show notifications, registers the app with the Apple Push Notification service (APNs), hands the device token to ActionScript and forwards notifications that arrive while the app is running.
Because macOS asks only once for permission, KlugPush also reports the current permission status and opens the Notifications page of System Settings, so a user who declined can switch notifications on later.
Notifications go straight from your server to APNs. No third-party push provider and no extra SDK are involved.
| Extension ID | com.klug.push.Push |
| Current version | 1.0.0 |
| Platforms | macOS (Apple silicon and Intel, one universal binary). On every other platform 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) |
| macOS | macOS 11 or later |
| Apple developer account | Membership in the Apple Developer Program, an App ID with the Push Notifications capability, and a Mac provisioning profile that contains it |
| Signing | The app bundle must be signed with the com.apple.developer.aps-environment entitlement and the matching provisioning profile |
| Server | Any server that can send HTTP/2 requests to APNs with a token-based (.p8) or certificate-based key |
| Permissions | The notification permission is requested at runtime by register(); 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>
KlugPush needs no InfoAdditions and no Android or iOS settings.
2 · Enable push for your App ID
- In your Apple developer account, open Certificates, Identifiers & Profiles → Identifiers and select the App ID of your Mac app. Its bundle ID is the <id> of your application descriptor.
- Enable the Push Notifications capability and save.
- Create or regenerate the Mac provisioning profile for this App ID (development profile for testing, distribution profile for the Mac App Store or Developer ID). A profile created before push was enabled does not contain it.
3 · Entitlements
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. The entitlement on macOS is called com.apple.developer.aps-environment; the iOS key aps-environment has no effect on the Mac.
4 · Sandbox or production
APNs has two environments, and a 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 | Builds for the Mac App Store, TestFlight or Developer ID distribution. 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.
Getting started
The smallest working setup: check support, 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 ) // true on macOS only { 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 ); }
On the first start macOS shows its permission prompt. The device token arrives whatever the user answers there: the permission only controls whether notifications are displayed.
Guides
Register and keep the token up to date
Call register() on every app start. APNs normally returns the same token, but it can change, for example after the app was reinstalled or the Mac was restored from a backup. Compare it with the token you sent last and update your server only when it changed. Calling register() again later in the same session is safe; the callbacks you pass replace the earlier ones.
The token is a lowercase hexadecimal string of the bytes APNs returns (currently 64 characters). The getter token keeps the last value, so code that runs later, for example after a sign-in, can read it without registering again.
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: use their own push solution 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 cause: the build is not signed with the push entitlement and a matching profile 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 ); }
Receive notifications while the app is running
onNotification receives the complete payload your server sent, as a JSON string: the aps dictionary plus all your own keys. It fires when a notification is delivered while the app is running. When the app is in the foreground, macOS normally does not show a banner for it, so this callback is the place to react.
When the app is not running, macOS shows the notification in Notification Center. Clicking it starts the app, but the payload of that notification is not passed to ActionScript; load the current state from your server at startup instead.
private function onNotification( json:String ):void { var payload:Object; try { payload = JSON.parse( json ); } catch ( e:Error ) { return; } // standard part var alert:Object = payload.aps ? payload.aps.alert : null; var text:String = ( alert is String ) ? String( alert ) : ( alert ? alert.body : "" ); // your own keys, e.g. { "aps": {...}, "eventId": "1234" } if ( payload.hasOwnProperty( "eventId" ) ) showEvent( payload.eventId, text ); // your app function }
Permission status and System Settings
macOS shows the permission prompt only once. After the user declined, register() cannot ask again; the only way back is the Notifications page in System Settings. KlugPush gives you the status so that your app can show the right hint:
| -1 | Unknown: neither register() nor refreshStatus() has delivered a status yet |
| 0 · STATUS_NOT_DETERMINED | The user has not been asked yet |
| 1 · STATUS_DENIED | Notifications are switched off for the app |
| 2 · STATUS_AUTHORIZED | Notifications are allowed |
| 3, 4 | Also possible from refreshStatus(): provisional and ephemeral authorization as reported by macOS (no constants) |
refreshStatus(onStatus) reads the live status asynchronously. The callback is stored: it is called with the result, and again whenever register() receives the answer to its prompt (then always 1 or 2). authStatus returns the last value without asking the system.
import com.klug.push.Push; import flash.desktop.NativeApplication; import flash.events.Event; private var push:Push = Push.getInstance(); public function initSettingsScreen():void { push.refreshStatus( showStatus ); // the user may change the setting in System Settings: read it again on return NativeApplication.nativeApplication.addEventListener( Event.ACTIVATE, onActivate ); } private function onActivate( e:Event ):void { push.refreshStatus( showStatus ); } private function showStatus( status:int ):void { switch ( status ) { case Push.STATUS_AUTHORIZED: setSwitch( "on" ); break; // your app UI case Push.STATUS_DENIED: setSwitch( "off" ); break; case Push.STATUS_NOT_DETERMINED: setSwitch( "ask" ); break; default: setSwitch( "on" ); // 3, 4: limited authorization } } public function onSwitchClicked():void { if ( push.authStatus == Push.STATUS_NOT_DETERMINED ) push.register( onToken, onPushError, onNotification ); // shows the prompt else if ( push.authStatus == Push.STATUS_DENIED ) push.openSettings(); // the only way back }
Listen with events instead of callbacks
Every result is also dispatched as a StatusEvent.STATUS on the Push instance. Note the layout of these events: the level names the kind (Push.TOKEN, Push.ERROR, Push.NOTIFICATION), and the code carries the value.
import com.klug.push.Push; import flash.events.StatusEvent; var push:Push = Push.getInstance(); push.addEventListener( StatusEvent.STATUS, onPushStatus ); push.register(); // all callbacks are optional function onPushStatus( e:StatusEvent ):void { switch ( e.level ) { case Push.TOKEN: sendTokenToServer( e.code ); break; // hex token case Push.ERROR: trace( "push error: " + e.code ); break; case Push.NOTIFICATION: handlePush( JSON.parse( e.code ) ); break; // JSON payload } }
The permission status is delivered only to the refreshStatus() callback, not as an event.
Send a test notification
To send notifications, 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":"Hello","body":"Test from my server"},"sound":"default"},"eventId":"1234"}' \ https://api.sandbox.push.apple.com/3/device/DEVICE_TOKEN_FROM_ONTOKEN
apns-topic is the bundle ID of your Mac app. A response with status 200 means APNs accepted the notification. 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.
Cross-platform apps
The extension contains a fallback for every platform other than macOS. There isSupported is false, register() returns false, and the other methods do nothing, so the same code can stay in an app that also builds for Windows, iOS or Android. Use the push solution of those platforms behind the isSupported check.
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 macOS, false on all other platforms. It does not check signing or entitlements. |
| token | public function get token():String The last device token received in this session, or null until registration has delivered one. |
| authStatus | public function get authStatus():int The last known permission status (see the status constants); -1 until register() or refreshStatus() has delivered one. Does not query the system. |
| register() | public function register(onToken:Function = null, onError:Function = null, onNotification:Function = null):Boolean Asks for permission to show alerts, sounds and badges (prompt only on the first time), then registers with APNs. onToken(token:String) receives the device token, onError(message:String) the system's message if registration fails, onNotification(json:String) the payload of each notification delivered while the app runs. The callbacks replace those of an earlier call. Returns true if the request was handed to the system, false where push is not supported. Asynchronous. |
| refreshStatus() | public function refreshStatus(onStatus:Function = null):void Reads the live permission status. onStatus(status:int) is called asynchronously with the result and is kept for the answer to a later register() prompt. Updates authStatus. No-op where not supported. |
| openSettings() | public function openSettings():void Opens System Settings at Notifications. No-op where not supported. |
| TOKEN | public static const TOKEN:String = "token" StatusEvent level: the code is the device token. |
| ERROR | public static const ERROR:String = "error" StatusEvent level: the code is the error message. |
| NOTIFICATION | public static const NOTIFICATION:String = "notification" StatusEvent level: the code is the notification payload as JSON. |
| STATUS_NOT_DETERMINED | public static const STATUS_NOT_DETERMINED:int = 0 The user has not been asked yet. |
| STATUS_DENIED | public static const STATUS_DENIED:int = 1 Notifications are switched off for the app. |
| STATUS_AUTHORIZED | public static const STATUS_AUTHORIZED:int = 2 Notifications are allowed. |
Events
All results arrive asynchronously, on the main thread of your app, both through the callbacks of register() and as flash.events.StatusEvent.STATUS on the Push instance.
| level "token" | Push.TOKEN. Fires after register() when APNs has issued the device token. code = the token as a lowercase hex string. Also calls onToken and updates token. |
| level "error" | Push.ERROR. Fires when registration fails, for example without the push entitlement. code = the system's error message. Also calls onError. |
| level "notification" | Push.NOTIFICATION. Fires when a notification is delivered while the app is running. code = the full payload as JSON. Also calls onNotification. |
| onStatus(status:int) | Callback only (no event). Fires with the result of refreshStatus(), and with 1 or 2 when the user answers the prompt of register(). |
Troubleshooting
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.
The user declined and the prompt never appears again
This is how macOS works. Check authStatus or call refreshStatus(); if it is STATUS_DENIED, explain the situation and call openSettings(). Read the status again when your app becomes active.
authStatus is -1
The status is cached only after register() or refreshStatus() has delivered it, and both answer asynchronously. Use the onStatus callback instead of reading the getter right after the call.
I listen for StatusEvents, but e.code is not "token"
For this extension the kind is in e.level and the value is in e.code. Compare e.level with Push.TOKEN, Push.ERROR and Push.NOTIFICATION.
A notification arrived but no banner was shown
When your app is in the foreground, macOS normally delivers the notification to the app without showing a banner. Handle it in onNotification. If the app is in the background and still no banner appears, check the notification settings of the app in System Settings (alert style, Focus modes).
The payload of a clicked notification is missing
onNotification covers notifications delivered while the app is running. A click on a notification that started the app is not forwarded. Fetch the current state from your server at startup.
isSupported is true, but push does not work in my test build
isSupported only tells you that the native part is present (macOS). Signing problems show up later, through onError.
Changelog
1.0.0
- Permission status: authStatus, refreshStatus() and the status constants
- openSettings() opens System Settings at Notifications
- First release: 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