# Install and integrate Airship Mobile & Web SDKs Install and integrate Airship SDKs for iOS, Android, Web, React Native, Flutter, and more. Browse by platform, or implement features using [SDK Topics]({{< ref "/sdk-topics/" >}}). ## Android Integrate the Airship SDK into your mobile applications for Android, Android TV, FireOS, and FireTV. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/android/message-center/embedding/#handling-display-requests) and [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/android/preference-center/embedding/#handling-display-requests). #### Kotlin Set the deep link listener during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/): ```kotlin Airship.deepLinkListener = DeepLinkListener { deepLink: String -> // Handle deep link true } ``` #### Java Set the deep link listener during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/): ```java Airship.setDeepLinkListener(deepLink -> { // Handle the deepLink return true; }); ``` # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. The SDK provides asynchronous access to feature flags using Kotlin suspend functions, which is intended to be called from a coroutine. For more information, see [Coroutines Overview guide](https://kotlinlang.org/docs/coroutines-overview.html). #### Kotlin ```kotlin // Get the FeatureFlag result val result: Result = FeatureFlagManager.shared().flag("YOUR_FLAG_NAME") // Check if the app is eligible or not if (result.getOrNull()?.isEligible == true) { // Do something with the flag } else { // Disable feature or use default behavior } ``` #### Java ```java // Get the FeatureFlag FeatureFlag featureFlag = FeatureFlagManager.shared().flagAsPendingResult("YOUR_FLAG_NAME").getResult(); // Check if the app is eligible or not if (featureFlag != null && featureFlag.isEligible()) { // Do something with the flag } else { // Disable feature or use default behavior } ``` ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/android/data-collection/privacy-manager/). #### Kotlin ```kotlin FeatureFlagManager.shared().trackInteraction(featureFlag) ``` #### Java ```java FeatureFlagManager.shared().trackInteraction(featureFlag) ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. #### Kotlin ```kotlin FeatureFlagManager.shared().flag("YOUR_FLAG_NAME").fold( onSuccess = { flag -> // do something with the flag }, onFailure = { error -> // do something with the error } ) ``` #### Java ```java FeatureFlag featureFlag = FeatureFlagManager.shared().flagAsPendingResult("YOUR_FLAG_NAME").getResult(); if (featureFlag == null) { // error } else if (featureFlag.isEligible()) { // Do something with the flag } ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Action Situations Actions are triggered with extra context in the form of a Situation. The different situations allow actions to determine if they should run, and may perform different behavior depending on the situation. | Description | Android | |-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| | Action was invoked manually. | [.SITUATION_MANUAL_INVOCATION](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-m-a-n-u-a-l_-i-n-v-o-c-a-t-i-o-n/index.html) | | Action was invoked from a launched push notification. | [.SITUATION_PUSH_OPENED](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-p-u-s-h_-o-p-e-n-e-d/index.html) | | Action is triggered when a notification is opened. | [.SITUATION_PUSH_OPENED](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-p-u-s-h_-o-p-e-n-e-d/index.html) | | Action was invoked from JavaScript or a URL. | [.SITUATION_WEB_VIEW_INVOCATION](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-w-e-b_-v-i-e-w_-i-n-v-o-c-a-t-i-o-n/index.html) | | Action was invoked from a foreground interactive notification button. | [.SITUATION_FOREGROUND_NOTIFICATION_ACTION_BUTTON](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-f-o-r-e-g-r-o-u-n-d_-n-o-t-i-f-i-c-a-t-i-o-n_-a-c-t-i-o-n_-b-u-t-t-o-n/index.html) | | Action was invoked from a background interactive notification button. | [.SITUATION_BACKGROUND_NOTIFICATION_ACTION_BUTTON](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-b-a-c-k-g-r-o-u-n-d_-n-o-t-i-f-i-c-a-t-i-o-n_-a-c-t-i-o-n_-b-u-t-t-o-n/index.html) | | Action was invoked from automation. | [.SITUATION_AUTOMATION](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-a-u-t-o-m-a-t-i-o-n/index.html) | ## Action Registry The action registry is the central place to register actions by name. Each entry in the registry contains an action, the names that the action is registered under, a predicate that allows filtering when an action should run, and allows specifying alternative actions for different situations. #### Kotlin ```kotlin Airship.actionRegistry.registerEntry(setOf("my_action_name", "my_alias")) { ActionRegistry.Entry(action = CustomAction()) } ``` #### Java ```java Airship.getActionRegistry().registerEntry(Set.of("my_action_name", "my_alias"), () -> { return new ActionRegistry.Entry(new CustomAction()); }); ``` #### Kotlin ```kotlin val entry = Airship.actionRegistry.getEntry("my_action_name") ``` #### Java ```java ActionRegistry.Entry entry = Airship.getActionRegistry().getEntry("my_action_name"); ``` #### Kotlin ```kotlin // Predicate that will reject PUSH_RECEIVED, causing the action to never run during that situation. val rejectPushReceivedPredicate: ActionPredicate = object : ActionPredicate { override fun apply(arguments: ActionArguments): Boolean { return SITUATION_PUSH_RECEIVED != arguments.situation } } // Update the entry with a new predicate Airship.actionRegistry.updateEntry("my_action_name", predicate = rejectPushReceivedPredicate) ``` #### Java ```java // Predicate that will reject PUSH_RECEIVED, causing the action to never run during that situation. ActionPredicate rejectPushReceivedPredicate = new ActionPredicate() { @Override public boolean apply(ActionArguments arguments) { return !(SITUATION_PUSH_RECEIVED.equals(arguments.getSituation())); } }; // Update the entry with a new predicate Airship.getActionRegistry().updateEntry("my_action_name", null, Collections.emptyMap(), rejectPushReceivedPredicate); ``` ## Triggering Actions In addition to triggering actions from messages, you can trigger them programmatically. #### Kotlin ```kotlin // Running an action directly through the ActionRunRequest ActionRunRequest.createRequest("actionName") .setSituation(SITUATION_MANUAL_INVOCATION) .setValue("actionValue") .run() // Running an action by registered name ActionRunRequest.createRequest("my_action_name") .setValue("actionValue") .run() // An optional callback when finished ActionRunRequest.createRequest("my_action_name") .setValue("actionValue") .run { arguments, result -> Logger.info("Action finished! Result: $result") } // Block until the action finishes val result = ActionRunRequest.createRequest("my_action_name").runSync() ``` #### Java ```java // Running an action directly through the ActionRunRequest ActionRunRequest.createRequest("actionName") .setSituation(Situation.MANUAL_INVOCATION) .setValue("actionValue") .run(); // Running an action by registered name ActionRunRequest.createRequest("my_action_name") .setValue("actionValue") .run(); // An optional callback when finished ActionRunRequest.createRequest("my_action_name") .setValue("actionValue") .run(new ActionCompletionCallback() { public void onFinish(ActionArguments arguments, ActionResult result) { Logger.info("Action finished! Result: " + result); } }); // Block until the action finishes ActionResult result = ActionRunRequest.createRequest("my_action_name").runSync(); ``` ## Custom Actions The action framework supports any custom actions. Create an action by extending the `Action` base class on Android. After `takeOff`, register the action. The action can be triggered the same way as built-in actions. #### Kotlin ```kotlin class CustomAction : Action() { override fun acceptsArguments(arguments: ActionArguments): Boolean { if (!super.acceptsArguments(arguments)) { return false } // Do any argument inspections. The action will stop // execution if this method returns false. return true } override fun perform(arguments: ActionArguments): ActionResult { Log.i("CustomAction", "Action is performing!") return ActionResult.newEmptyResult() } } ``` #### Java ```java public class CustomAction extends Action { @Override public boolean acceptsArguments(ActionArguments arguments) { if (!super.acceptsArguments(arguments)) { return false; } // Do any argument inspections. The action will stop // execution if this method returns false. return true; } @Override public ActionResult perform(ActionArguments arguments) { Log.i("CustomAction", "Action is performing!"); return ActionResult.newEmptyResult(); } } ``` > **Note:** On Android, custom actions may override the `shouldRunOnMainThread()` method to specify whether the action should > be run on the main tread, or on a background thread. Implementations should take care to avoid long-running tasks, > especially when running on the main thread. # Live Updates > Integrate Live Updates into your Android app to display real-time updates in notifications, widgets, or within your app. {{< badge "axp" >}} For the push API method, see the [Android Live Updates](https://www.airship.com/docs/guides/messaging/features/android-live-updates/) messaging guide. See also the [Android Live Updates](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ## Installation Include the Live Updates module in your app's dependencies: #### Gradle Kotlin **app build.gradle.kts** ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-live-update:$airshipVersion") } ``` #### Gradle Groovy **app build.gradle** ```groovy dependencies { def airshipVersion = "androidSdkVersion" implementation "com.urbanairship.android:urbanairship-live-update:$airshipVersion" } ``` ## Creating a handler The Airship SDK supports two types of Live Update handlers: * `NotificationLiveUpdateHandler` — Displays a notification with a custom layout, with content updated by the Live Update. * `CustomLiveUpdateHandler` — Receives Live Update events and provides flexibility to display content using a custom implementation. This can be used to power home screen widgets, views embedded in the app, and more. Each handler type has two different interfaces that may be implemented, to support suspending or callback-based code: * `SuspendLiveUpdateNotificationHandler` * `CallbackLiveUpdateNotificationHandler` * `SuspendLiveUpdateCustomHandler` * `CallbackLiveUpdateCustomHandler` The following `SampleLiveUpdateHandler` reads content from the Live Update payload and displays scores for a sports game in a custom notification layout, using `RemoteViews`: #### Kotlin ```kotlin class SampleLiveUpdateHandler : SuspendLiveUpdateNotificationHandler() { override suspend fun onUpdate( context: Context, event: LiveUpdateEvent, update: LiveUpdate ): LiveUpdateResult { // Read content_state fields from the Live Update payload val teamOneScore = update.content.opt("team_one_score").getInt(0).toString() val teamTwoScore = update.content.opt("team_two_score").getInt(0).toString() val statusUpdate = update.content.opt("status_update").optString() // Expanded notification layout val bigLayout = RemoteViews(context.packageName, R.layout.sports_big).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) setTextViewText(R.id.statusUpdate, statusUpdate) } // Collapsed notification layout val smallLayout = RemoteViews(context.packageName, R.layout.sports_small).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) } // Create the notification builder val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_EVENT) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(smallLayout) .setCustomBigContentView(bigLayout) // Return 'ok' with the notification builder. // The Airship SDK will handle posting the notification. // Returning LiveUpdateResult.cancel() will end the Live Update and dismiss the notification. return LiveUpdateResult.ok(builder) } companion object { private const val NOTIFICATION_CHANNEL_ID = "sports" } } ``` #### Java ```java public class SampleLiveUpdateHandler extends CallbackLiveUpdateNotificationHandler() { @Override public void onUpdate( Context context, LiveUpdateEvent event, LiveUpdate update, LiveUpdateResultCallback callback ) { // Read content_state fields from the Live Update payload int teamOneScore = update.getContent().optInt("team_one_score", 0); int teamTwoScore = update.getContent().optInt("team_two_score", 0); String statusUpdate = update.getContent().optString("status_update"); // Expanded notification layout RemoteViews bigLayout = new RemoteViews(context.getPackageName(), R.layout.sports_big); bigLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore)); bigLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore)); bigLayout.setTextViewText(R.id.statusUpdate, statusUpdate); // Collapsed notification layout RemoteViews smallLayout = new RemoteViews(context.getPackageName(), R.layout.sports_small); smallLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore)); smallLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore)); // Create the notification builder NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_EVENT) .setStyle(new NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(smallLayout) .setCustomBigContentView(bigLayout); // Return 'ok' with the notification builder. // The Airship SDK will handle posting the notification. // Returning LiveUpdateResult.cancel() will end the Live Update and dismiss the notification. callback.onResult(LiveUpdateResult.ok(builder)); } private static final String NOTIFICATION_CHANNEL_ID = "sports"; } ``` ### Registering a handler Handlers must be registered with `LiveUpdateManager` in order to receive Live Update events. This should be done *once* after `takeOff`. In your `Autopilot` class, or in `Application.onCreate`, register the types. #### Kotlin ```kotlin class SampleAutopilot : Autopilot() { override fun onAirshipReady(context: Context) { Airship.liveUpdateManager.register( type = "notification", handler = SampleLiveUpdateHandler() ) } } ``` #### Java ```java public class SampleAutopilot extends Autopilot() { @Override public void onAirshipReady(Context context) { LiveUpdateManager.shared().register("notification", new SampleLiveUpdateHandler()); } } ``` > **Note:** The `type` used above, `"notification"`, is used to map Live Update events to the corresponding handler in your app. > The value can be any string that is unique across all handlers registered by an app. This also allows a single handler to manage multiple Live Updates that each have a unique `name`. ### Starting Live Updates Live Updates can be started from within the app. #### Kotlin ```kotlin Airship.liveUpdateManager.start( name = "sports-game-123", type = "notification", content = jsonMapOf( "team_one_score" to 0, "team_two_score" to 0, "status_update" to "Game started!" ) ) ``` #### Java ```java Map content = new HashMap<>(); content.put("team_one_score", 0); content.put("team_two_score", 0); content.put("status_update", "Game started!"); LiveUpdateManager.shared().start( "sports-game-123", "notification", content ); ``` ### Updating Live Updates Live Updates can be updated from within the app. #### Kotlin ```kotlin Airship.liveUpdateManager.update( name = "sports-game-123", content = jsonMapOf( "team_one_score" to 3, "team_two_score" to 0, "status_update" to "Game started!" ) ) ``` #### Java ```java Map content = new HashMap<>(); content.put("team_one_score", 3); content.put("team_two_score", 0); content.put("status_update", "Game started!"); LiveUpdateManager.shared().update( "sports-game-123", content ); ``` ### Ending Live Updates You can end a Live Update from within the app. #### Kotlin ```kotlin Airship.liveUpdateManager.stop( name = "sports-game-123", content = jsonMapOf( "team_one_score" to 9, "team_two_score" to 6, "status_update" to "Game over!" ) ) ``` #### Java ```java Map content = new HashMap<>(); content.put("team_one_score", 9); content.put("team_two_score", 6); content.put("status_update", "Game over!"); LiveUpdateManager.shared().stop( "sports-game-123", content ); ``` ### Clearing all active Live Updates During development, it can be useful to reset Live Update tracking on app launch. This allows any Live Updates to be started fresh, even if they were already started during a previous launch. To end all currently active Live Updates, call the `clearAll()` method on `LiveUpdateManager`. #### Kotlin ```kotlin Airship.liveUpdateManager.clearAll() ``` #### Java ```java LiveUpdateManager.shared().clearAll(); ``` # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. Analytics allows you to track user engagement and app performance through custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/android/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with [Custom Events](https://www.airship.com/docs/reference/glossary/#custom_event). They require enabling analytics for your app. #### Kotlin ```kotlin customEvent("event_name") { addProperty("my_custom_property", "some custom value") addProperty("is_neat", true) addProperty("any_json", jsonMapOf("foo" to "bar")) }.track() ``` #### Java ```java CustomEvent.newBuilder("event_name") .setEventValue(123.12) .addProperty("my_custom_property", "some custom value") .addProperty("is_neat", true) .addProperty("any_json", JsonMap.newBuilder() .put("foo", "bar") .build()) .build() .track(); ``` ### Templates Custom Event Templates are a wrapper for Custom Events and are available for Android, [iOS](https://www.airship.com/docs/developer/sdk-integration/apple/analytics/#templates), and [Web](https://www.airship.com/docs/developer/sdk-integration/web/analytics-and-reporting/#templates). See also [CustomEvent](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.analytics/-custom-event/index.html) in the Android SDK library. #### Account Use this template to create Custom Events for account-related events. The template is written with account registration as the example. #### Kotlin Track a registered account event: ```kotlin customEvent(AccountEventTemplate.Type.REGISTERED) { }.track() ``` With optional properties: ```kotlin customEvent( type = AccountEventTemplate.Type.REGISTERED, properties = AccountEventTemplate.Properties( category = "premium" ) ) { setEventValue(9.99) setTransactionId("12345") }.track() ``` #### Media Use this template to create Custom Events for media-related events, including consuming, browsing, starring, and sharing content. #### Kotlin Track a consumed content event: ```kotlin customEvent(MediaEventTemplate.Type.Consumed) { }.track() ``` With an optional value: ```kotlin customEvent(MediaEventTemplate.Type.Consumed) { setEventValue(1.99) }.track() ``` With optional properties: ```kotlin customEvent( type = MediaEventTemplate.Type.Consumed, properties = MediaEventTemplate.Properties( id = "12345", category = "entertainment", type = "video", eventDescription = "Watching latest entertainment news.", author = "UA Enterprises", isFeature = true, publishedDate = "August 25, 2016" ) ) { setEventValue(2.99) }.track() ``` #### Kotlin Track a starred content event: ```kotlin customEvent(MediaEventTemplate.Type.Starred) { }.track() ``` With optional properties: ```kotlin customEvent( type = MediaEventTemplate.Type.Starred, properties = MediaEventTemplate.Properties( id = "12345", category = "entertainment", type = "video", eventDescription = "Watching latest entertainment news.", author = "UA Enterprises", isFeature = true, publishedDate = "August 25, 2016" ) ) { setEventValue(2.99) }.track() ``` #### Kotlin Track a browsed content event: ```kotlin customEvent(MediaEventTemplate.Type.Browsed) { }.track() ``` With optional properties: ```kotlin customEvent( type = MediaEventTemplate.Type.Browsed, properties = MediaEventTemplate.Properties( id = "12345", category = "entertainment", type = "video", author = "UA Enterprises", isFeature = true, publishedDate = "August 25, 2016" ) ) { }.track() ``` #### Kotlin Track a shared content event: ```kotlin customEvent(MediaEventTemplate.Type.Shared()) { }.track() ``` With a source and medium: ```kotlin customEvent( MediaEventTemplate.Type.Shared(source = "facebook", medium = "social") ) { }.track() ``` With optional properties: ```kotlin customEvent( type = MediaEventTemplate.Type.Shared(source = "facebook", medium = "social"), properties = MediaEventTemplate.Properties( id = "12345", category = "entertainment", type = "video", eventDescription = "Watching latest entertainment news.", author = "UA Enterprises", isFeature = true, publishedDate = "August 24, 2016" ) ) { }.track() ``` #### Retail Use this template to create Custom Events for retail-related events, including browsing a product, adding an item to a cart, purchasing an item, starring a product, and sharing a product. #### Kotlin Track a purchased event: ```kotlin customEvent(RetailEventTemplate.Type.Purchased) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.Purchased, properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` #### Kotlin Track a browsed event: ```kotlin customEvent(RetailEventTemplate.Type.Browsed) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.Browsed, properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` #### Kotlin Track an added-to-cart event: ```kotlin customEvent(RetailEventTemplate.Type.AddedToCart) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.AddedToCart, properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` #### Kotlin Track a starred product event: ```kotlin customEvent(RetailEventTemplate.Type.Starred) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.Starred, properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` #### Kotlin Track a shared product event: ```kotlin customEvent(RetailEventTemplate.Type.Shared()) { }.track() ``` With a source and medium: ```kotlin customEvent( RetailEventTemplate.Type.Shared(source = "facebook", medium = "social") ) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.Shared(source = "facebook", medium = "social"), properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. #### Kotlin ```kotlin Airship.analytics.editAssociatedIdentifiers { addIdentifier("key", "value") } ``` #### Java ```java Airship.getAnalytics() .editAssociatedIdentifiers() .addIdentifier("key", "value") .apply(); ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. #### Kotlin ```kotlin Airship.analytics.trackScreen("MainScreen") ``` #### Java ```java Airship.getAnalytics().trackScreen("MainScreen"); ``` # Android SDK Changelog > The latest updates to the Airship Android SDK. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 20.7.2 May 13, 2026 Patch release that fixes a couple of Scene issues and improves remote data refresh behavior for kiosk-style apps. Apps that make use of SMS inputs in Scenes should update to this version or newer. ### Changes - Fixed SMS text input in Scenes - Updated Scene label and label button to improve rendering consistency - Improved remote data refresh behavior for kiosk-style apps that stay foregrounded for long periods of time ## 20.7.1 May 8, 2026 Patch release that includes minor API changes to allow for email and SMS registration in Airship cross-platform frameworks. ### Changes - Internal API changes to support email and SMS registration in frameworks. ## 20.7.0 April 30, 2026 Minor release that adds support for Native Message Center. ### Changes - Added support for rendering Native Content in Message Center. ## 20.6.4 April 24, 2026 Patch release that fixes keyboard resize handling in modal scenes. ### Changes - Fixed modal scenes so content resizes correctly when the soft keyboard appears, closing a gap between content and the keyboard on older API levels ## 20.6.3 April 20, 2026 Patch release with several push reliability improvements. ### Changes - Fixed a race condition in `PushManager` that could cause false push opt-outs when FCM tokens rotate or registration fails transiently - Fixed `SQLiteBlobTooBigException` errors in `PreferenceDataStore` for large stored values - Fixed invalid JSON logging - Fixed unnecessary backoff when Airship's WorkManager jobs are cancelled externally ## 20.6.2 April 15, 2026 Patch release that hardens against a specific WebView crash that can occur on certain Android 16 devices. ### Changes - Avoid crashing if WebView inflation fails in `HtmlActivity`, which displays Custom HTML IAX messages, when we encounter a known issue on Android 16 that primarly impacts Samsung devices (https://issuetracker.google.com/issues/448359671) ## 20.6.1 March 27, 2026 Patch release that fixes a dependency resolution issue with the FCM module introduced in 20.4.0. Apps that depend on `urbanairship-fcm` and reference Firebase Messaging classes directly should update to this version. ### Changes - Fixed `firebase-messaging` dependency not being available on the compile classpath ## 20.6.0 March 25, 2026 Minor release that extends Markdown support in Scenes and improves handling of navigation to invalid Message Center message IDs. ### Changes - Added superscript and subscript Markdown support in Scenes (`^^superscript^^` and `,{subscript},`) - Updated Message Center to show the message view with an error when attempting to open a message with an invalid message ID, instead of failing to the Messages list ## 20.5.0 March 17, 2026 Minor release that improves video playback and pager navigation reliability in Scenes, along with several bug fixes. This release also includes updates to proguard rules to support behavior changes in AGP 9. Apps that have migrated to AGP 9.x should update to this version or newer. ### Changes - Improved video playback lifecycle handling in Scenes - Improvements for Scenes with complex branching - Fixed `Airship.takeOff` returning before `onReady` callbacks have completed - Fixed possible hang when calling `fetchMessages` on Message Center - Fixed Message Center inbox update notifications - Fixed Message Center message content type parsing - Fixed SMS validation error handling - Fixed overly frequent permission listener callbacks - Updated proguard rules to keep default constructors for Airship classes ## 20.4.0 March 4, 2026 Minor release with a pair of improvements for Scenes. ### Changes - Adjusted Markdown rendering in Scenes to be less aggressive when interpreting styling delimiters inside of words - Improved Scene border rendering when rounded corners are present ## 20.3.0 February 25, 2026 Minor release that adds support for Native Message Center. Native content type requires displaying the message content in an Airship Message View. Apps that do not use Airship's message views (e.g. using a WebView directly) should filter out messages where `message.contentType` is not `Message.ContentType.Html`. ### Changes - Removed library group restrictions on `PushProviderBridge`. - Added support for Native Message Center. ## 20.2.2 February 19, 2026 Patch release with an FCM availability check improvement to better handle unexpected Google Play service lookup failures. ### Changes - Added exception handling and logging around the FCM Google Play Store availability check to prevent unexpected crashes when Google checks fail. ## 20.2.1 February 6, 2026 Patch release with several minor improvements for the Compose Message Center UI. Apps that make use of the Compose Message Center should update to take advantage of these improvements. ### Changes - Allows the Compose Message Center toolbar title to be overridden via `MessageCenterOptions` - Option to disable message deletion in the Compose Message Center via `MessageCenterOptions` - Fixed the up arrow and `onNavigateUp` callback for the Compose Message Center list screen ## 19.13.8 January 28, 2026 Patch release that fixes an issue with custom events being double counted for IAX triggers (reporting was not affected). Apps that make use of custom event triggers in IAX should update to this version or later. ### Changes - Fixed issue that caused custom events being double counted for IAX triggers (reporting was not affected) ## 20.2.0 January 28, 2026 Minor release that adds a new `PreferenceCenterView`, fixes fetching subscription lists after changing contact IDs, and improvements for Scenes. ### Changes - Added `PreferenceCenterView` for easier integration of Preference Centers when the `Fragment` API is not desired - Fixed issue where subscription lists could remain cached after changing contact IDs - Improved measurement of videos inside of containers in Scenes - Improved checkbox and radio button accessibility in Scenes - Improved TalkBack navigation for Scenes with Pagers - Fixed issue that caused custom events being double counted for IAX triggers (reporting was not affected) ## 20.1.1 January 17, 2026 Patch release that fixes a potential image-related crash in Scenes and acessibility issues. ### Changes - Fixed a potential crash in Scenes with specific images and display settings - Fixed Message Center title not being marked as a heading - Fixed Scene icon buttons not having a proper disabled effect ## 19.13.7 January 16, 2026 Patch release that fixes a potential image-related crash in Scenes. ### Changes - Fixes a potential crash in Scenes with specific images and display settings. ## 20.1.0 January 9, 2026 Minor release that includes several fixes and improvements for Scenes, In-App Automations, and notification handling. ### Changes - Fixed a measurement issue with videos inside of containers in Scenes in certain configurations - Fixed a potential crash in `NotificationProxyActivity` - In-app automations and Scenes that were not available during app launch can now be triggered by events that happened in the previous 30 seconds - Added support for additional text styles in Scenes - Added highlight markdown support in Scenes (`==highlighted text==`) - Fixed incrementing frequency limits before a message is ready to display - Improved support for WebViews in Scenes - Added support for Story pause/resume and back/next controls ## 20.0.7 December 30, 2025 Patch release with improvements to notification processing timing that resolves a crash when app is opened from a notification on Android 15. ### Changes - Fixed notification processing timing for Android 15 compatibility ## 20.0.6 December 16, 2025 Patch release to fix a regression in `NotificationIntentProcessor` that interfered with handling of `PendingIntent`s set on custom built notifications. ### Changes - Fixed issue with custom notification handling of `PendingIntent`s in `NotificationIntentProcessor` ## 20.0.5 December 5, 2025 Patch release that fixes an issue with opening the Compose Message Center. ### Changes - Fixed opening of Compose MessageCenterActivity - Merged PushManagerExtensions into PushManager ## 20.0.4 November 25, 2025 Patch release that fixes a potential race condition when setting metadata and creating action arguments concurrently. Apps experiencing crashes when processing push notifications should update to resolve this issue. ### Changes - Fixed potential `ConcurrentModificationException` in `ActionRunRequest` when metadata is modified concurrently with action execution, most likely occurring when processing incoming push notifications ([#258](https://github.com/urbanairship/android-library/issues/258)). ## 20.0.3 November 14, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes and minor fixes for the Preference Center Compose module. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Fixed YouTube video embedding to comply with YouTube API Client identification requirements. - Allow multiple Preference Centers to be displayed with Preference Center Compose. - Fixed checked/unchecked icon assets for Preference Center Compose. - Updated Preference Center Compose default toolbar to allow `navIcon` to be `null`. ## 19.13.6 November 14, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Fixed YouTube video embedding to comply with YouTube API Client identification requirements. ## 20.0.2 November 4, 2025 Patch release that fixes prompting for permissions on foreground. ### Changes - Fixed prompting for permissions on foreground. - Removed usage of material icons compose library. - Updated Message Center titles to be markes as headings. ## 20.0.1 October 24, 2025 Minor release that fixes packaging and publishing for the modules added in 20.0.0. Apps upgrading to SDK 20.x should update directly to 20.0.1 to ensure proper packaging of these modules. ### Changes - Fixed publishing for: - `urbanairship-message-center-core` - `urbanairship-message-center-compose` - `urbanairship-preference-center-core` - `urbanairship-preference-center-compose` - `urbanairship-debug` ## 20.0.0 October 23, 2025 Major SDK release with several breaking changes. See the [Migration Guide](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-19-20.md) for detailed instructions on upgrading. ### Changes - compileSdkVersion updated to 36 - Kotlin updated to 2.2.0 - The `UAirship` singleton has been deprecated and replaced with `Airship` - `Airship` is no longer a shared instance; instead, it exposes static methods for accessing components - Majority of the SDK has been migrated to Kotlin - Message Center package changes: - `message-center-core`: Core API with no UI - `message-center`: Android XML layouts (depends on `message-center-core`) - `message-center-compose`: New Jetpack Compose UI (depends on `message-center-core`) - Preference Center package changes: - `preference-center-core`: Core API with no UI - `preference-center`: Android XML layouts (depends on `preference-center-core`) - `preference-center-compose`: New Jetpack Compose UI (depends on `preference-center-core`) - New AirshipDebug package that exposes insights and debugging capabilities into the Airship SDK for development builds, providing enhanced visibility into SDK behavior and performance. ## 19.13.5 October 13, 2025 Patch release that handles BigDecimal in our JSON parsing. This prevents parse exceptions if the default Android org.json package is replaced by the org.json maven package. ### Changes - Handle BigDecimal and other number values when parsing JSON from a string. ## 19.13.4 October 6, 2025 Patch release that addresses an issue with handling Play Services errors before `takeOff` and fixes a Scene pager transition bug. ### Changes - Updated `PlayServiceErrorActivity` to handle play services errors before `takeOff` is called. - Fixed Scene pager issue where tapping the scene during a transition from one page to another would interrupt the transition. ## 19.13.3 September 26, 2025 Patch release that fixes an issue with handling `uairship://close` in Message Center and improves Scene accessibility. ### Changes - Fixed handling of `uairship://close` links in Message Center - Improved accessibility for Scene pager indicators - Improved `DeferredResult` logging ## 19.13.2 September 18, 2025 Patch release that adds more logs to the deferred schedules preparing process. ### Changes - Added more logs to the deferred schedules preparing process. ## 19.13.1 September 15, 2025 Patch release to fix an issue with showing out of date In-App Automations and Scenes. ### Changes - Fixed refreshing out of date In-App Automations and Scenes before displaying. ## 19.13.0 September 5, 2025 Minor release that adds support for handling `uairship://message_center/message/<message_id>` links to open a specific message in Message Center. ### Changes - Added support for handling `uairship://message_center/message/<message_id>` links to Message Center ## 19.12.0 September 4, 2025 Minor release that adds a new flag to HTML In-App message content to force full screen on all devices. ### Changes - Added `forceFullScreenDisplay` to HTML In-App message content - Improved accessibility in Scenes by removing labels from being focusable when using keyboard navigation ## 19.11.0 August 21, 2025 Minor release that enforces that incoming pushes are for the current channel ID and adds a manifest metadata entry to control handling of insets for IAM banners for edge-to-edge mode. ### Changes - Added Activity metadata entry (`com.urbanairship.iam.banner.BANNER_INSET_EDGE_TO_EDGE`) to force handling of insets for IAM banners in edge-to-edge mode. - Channel ID is now enforced for incoming pushes, ensuring that only pushes for the current channel ID are processed. ## 18.7.2 August 19, 2025 Patch release that fixes embedded display reporting and a potential crash in Scenes. Apps that use Scenes or Embedded Content should update to this version or later. ### Changes - Fixed an issue that could cause embedded content displays to be reported too early. - Fixed a potential crash that can occur when a Scene is dismissed. ## 19.10.2 August 13, 2025 Patch release that fixes embedded display reporting and a potential crash in Scenes. Apps that use Scenes or Embedded Content should update to this version or later. ### Changes - Fixed an issue that could cause embedded content displays to be reported too early. - Fixed a potential crash that can occur when a Scene is dismissed. ## 19.10.1 August 1, 2025 A patch release that fixes an automation dao crash if an expected nonnull JSON field contains invalid JSON. ### Changes - Fixed potential automation dao crash when an expected nonnull JSON field contains invalid JSON. ## 19.10.0 July 24, 2025 A minor release with accessibility and layout improvements to Scenes, a key performance update, and several bug fixes. ### Changes - Added support in Scenes for linking form inputs to a label for better accessibility. - Added container item alignment to Scenes to change the natural alignment within a container. - Updated the initial remote-data request (IAX, Config, Feature Flags, etc...) to bypass work manager to improve performance. - Fixed setting content-descriptions on a text/number/email input in Scenes to provide better accessibility. - Fixed potential automation dao crash when migrating from an older SDK version. - Fixed an issue where dismissing a Scene with a back gesture could prevent it from displaying again in the same session. ## 19.9.2 July 14, 2025 Patch release with several fixes and accessibility improvements for Scenes. ### Changes - Fixed a crash when dismissing an in-app automation view. - Fixed multiple page views being recorded for pages in branching Scenes. - Fixed a bug in Message Center Message WebView that could potentially interfere with JS in other web views. - Accessibility fixes and improvements for Scenes. ## 19.9.1 June 24, 2025 Patch release that enhances logging, fixes a potential memory leak in paging Scenes, and improves accessibility. ### Changes - Fixed potential memory leak in paging Scenes by improving accessibility listener lifecycle management. - Added 'logPrivacyLevel' to the config to improve managing logging visibility. - Added accessibility dismissal announcement for in-app messages. ## 19.9.0 June 17, 2025 A minor update with enhancements to the Scenes and Message Center functionality and bug fixes for Analytics and Automation. This version is required for Scene branching and phone number collection. ### Changes Analytics: - Fixed bug that could cause locale-based descrepancies in reports. Automation: - Fixed version trigger predicate matching to properly evaluate app version conditions. Message Center: - Automatically retries failed message list refreshes for improved reliability. - Expired messages will no longer trigger a network request to refresh the listing. Scenes: - Fixed layout issues with modal frames, specifically related to margins and borders. - Fixed border rendering issues when stroke thickness exceeds corner radius. - Fixed several issues related to Scene branching. - Added support for custom corner radii on borders. - Added support for more flexible survey toggles. ## 19.8.0 May 23, 2025 Minor release focused on performance improvements for Scenes. ### Improvements - Improved load times for Scenes by prefetching assets concurrently. ## 19.7.0 May 15, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. - Fixed minor issue with SMS collection in Scenes where the button loading indicator does not clear if submission encounters an error. ## 19.6.2 April 29, 2025 Patch release that fixes a crash in `PushManager.onTokenChanged`, introduced in release 19.6.0. Apps should skip release 19.6.0 and 19.6.1 and update directly to this version, or later. ### Changes - Fixed nullability of `oldToken` in `PushManager.onTokenChanged`. ## 19.6.1 April 28, 2025 Patch release that fixes a crash with NPS scores within a Scene that uses branching. Apps planning on using the upcoming branching feature should update. ### Changes - Fixed crash with a branching Scene with an NPS widget. ## 19.6.0 April 24, 2025 Minor release adding branching and SMS support for Scenes. ### Changes - Added support for branching in Scenes. - Added support for phone number collection and registration in Scenes. - Added support for setting JSON attributes for Channels and Contacts. - Added a new `mutationsFlow` to `AddTagsAction` and `RemoveTagsAction` to expose tag mutations when applied. - Updated Message Center Inbox to refresh messages on app foreground. ## 19.5.1 April 17, 2025 Patch release with fix for regression in Contacts that could cause a failure to resolve a Contact ID when Contacts are disabled. ### Changes - Fixed Contact ID resolution when contacts are disabled. ## 19.5.0 March 31, 2025 Minor release that adds a public method `Inbox.deleteAllMessages()` and remove restrictions for subclassing `MessageWebView` and `MessageWebViewClient`. ### Changes - Added a new public method `Inbox.deleteAllMessages()` to delete all messages from Message Center. - Removed library group restrictions on `MessageWebView` and `MessageWebViewClient`. ## 19.4.0 March 24, 2025 Minor release that adds support for Custom View in Scenes and fixes Privacy Manager issues when disabling all features. ### Changes - Added Custom View support to enable showing App managed views within a Scene. - Fixed an issue where the Privacy Manager sent multiple opt-out requests after features were disabled following being enabled. ## 19.3.0 March 6, 2025 Minor release that fixes an issue with modal Scenes and adds support for hoisting `AirshipEmbeddedViewGroup` composable state. Apps that make use of Scenes should update to this version or greater. ### Changes - Fix a potential crash when displaying a modal Scene - Added support for hoisting `AirshipEmbeddedViewGroup` composable state ## 18.7.1 February 25, 2025 Patch release to fix a casting exception with Embedded Content. ### Changes - Fixed exception due to a bad cast when using Embedded Content. ## 18.6.1 February 25, 2025 Patch release to fix a casting exception with Embedded Content. ### Changes - Fixed exception due to a bad cast when using Embedded Content. ## 19.2.0 February 21, 2025 Minor release that includes improvements for Scenes and Feature Flags. ### Changes - Added a fade transition for Scenes - Added support for email registration in Scenes - Fixed issues with autoplay videos in Scenes - Improved image download and scaling logic - Fixed an issue with image pre-caching when unable to successfully download an image - Expose rule refresh status for Feature Flags ## 18.7.0 February 7, 2025 Minor release that updates AndroidX libraries. A `compileSdk` of 35+ is required. ### Changes - Updated several AndroidX dependencies - Updated to Kotlin 2.x ## 19.1.0 February 5, 2025 Minor release that fixes an issue with embedded view sizing in scrolling views, improves Message Center accessibility, and replaces usages of `Random` with `SecureRandom`. Apps that make use of Embedded Content or Message Center should update. ### Changes - Fixed an issue with embedded sizing in scrolling views - Improved Message Center Accessibility - Replaced usage of `Random` with `SecureRandom` - Made `MessageWebView` and `MessageWebViewClient` both `public` and `open` for subclassing - Exposed Message Center `ViewModel` state via `LiveData`, in addition to Kotlin `Flow`s - Added `PendingResult` based methods to `Inbox`, for getting read and unread message counts and listing all message IDs ## 19.0.0 January 17, 2025 Major release that adds support for Android 15 (API 35) and updates Message Center and Preference Center to use Material 3. Breaking changes in Message Center are included in this release. See the [Migration Guides](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-18-19.md) for more info. ### Changes - The Airship SDK now requires `compileSdk` version 35 (Android 15) or higher, and `minSdk` version 23 (Android 6) or higher. - Migrated Message Center APIs to Kotlin, using asynchronous access patterns. New suspend functions and Flows have been added for Kotlin, and Java APIs have been updated to use `PendingResult` or callbacks. - Rewrote the provided Message Center UI to follow modern Android UI conventions, use Material 3 theming, and support edge-to-edge mode for Android 15. - Updated Preference Center to use Material 3 theming and support edge-to-edge mode for Android 15. - Added `Feature.FEATURE_FLAGS` to `PrivacyManager` to control enablement of feature flags. - Added support for wrapping score views in Scenes. - Added support for Feature Flag experimentation. ## 18.6.0 December 19, 2024 Minor release that updates how Feature Flags are resolved, improves Scene rendering on Android 15, and fixes potential exceptions related to PermissionsManager and PermissionDelegates. ### Changes - Added `resultCache` to `FeatureFlagManager`. This cache is managed by the app and can be optionally used when resolving a flag as a fallback if the flag fails to resolve or if the flag rule set does not exist. - FeatureFlag resolution will now resolve a rule set even if the listing is out of date. - Improved Scene rendering on Android 15, for scenes that do not ignore safe areas. - Prevent potential "Already resumed" exceptions that could be caused by a permission delegate calling the callback multiple times. - Improved constraint version matching ## 18.5.0 December 5, 2024 Minor release that includes various improvements to scenes, data management and some minor bug fixes. ### Changes - Added support to mark a label as a heading in Scenes. - Improved live update database handling to mitigate rare filesystem crashes. - Improved automation store to avoid query limits. ## 18.4.2 November 26, 2024 Patch release that fixes an issue with Embedded Views being impacted by certain App theme customizations, avoids a potential NPE related to network failures, and adds more useful logging around Feature Flag evaluation. ### Changes - Prevent App-level theme customizations from impacting Embedded Views - Avoid a potential NPE related to network failures, when no error body is present - Improved logging around Feature Flag evaluation ## 18.4.1 November 16, 2024 Patch release that fixes an issue with pausing and resuming In-App Automations and avoids a potential crash in the Automation database. ### Changes * Fixed an issue with `AutomationEngine.setEnginePaused(...)` that could prevent message displays when paused an then un-paused * Fixed a potential crash in Automation DB if 1000+ rows are present in the schedules table ## 18.4.0 November 1, 2024 Minor release with several enhancements to Scenes and In-App Automations. ### Changes - Added shadow support for modal Scenes - Added new Scene layout to allow adding actions to anything within a Scene - Added new `AirshipEmbeddedViewGroup` composable to make it possible to show a carousel of embedded views for the same embedded ID - Improved accessibility of scene story indicator. Indicator has been updated to make it obvious which page is active by reducing the height of the inactive pages. Previously this was conveyed only through color - improved accessibility for In-App Automation views - Fixed issue with FCM registration if the FCM application is not configured before Airship starts, causing launch notifications to be ignored ## 18.3.3 October 16, 2024 Patch release that fixes a potential crash when displaying In-App Automation messages, improves WebView security, and improves accessibility in Scenes and Stories. Apps that make use of In-App Automation, Landing Pages, or Message Center should update. ### Changes - Fix a potential crash when displaying In-App messages - Explicitly disallow file and content access in all WebViews - Accessibility improvements for Scenes and Stories ## 18.3.2 October 3, 2024 Patch release that improves markdown support in Scenes and fixes for automation display interval and frequency limit handling. Apps that make use of markdown in Scenes, or automations with display intervals or frequency limits should update. ### Changes - Improve markdown support in Scenes, including better handling of newlines in the input text. - Fixed automation display interval and frequency limit handling. ## 18.3.1 September 30, 2024 Patch release that fixes modal IAA border radius and fixes scenes with wide images. ### Changes - Fixed modal IAA border radius. - Fixed scenes with wide images. ## 18.3.0 September 13, 2024 Minor release that adds a new method `enableUserNotifications(PermissionPromptFallback)` on `PushManager`. ### Changes - Added a `enableUserNotifications(PermissionPromptFallback)` method on `PushManager` that will attempt to enable notifications and use the fallback if the permission is denied. ## 18.2.0 September 6, 2024 Minor release with several enhancements to In-App Automation, Scenes, and Surveys. This version also contains a fix for applications that are targeting API 35. ### Changes - Updated compose bom to 2024.06.00. - Replaced the usage of `removeFirst` to avoid crashes when targeting API 35. - Added ability to customize the content per In-App Automation with the new `InAppMessageContentExtender`. - Added plain markdown support for text markup in Scenes. - Added execution window support to In-App Automation, Scenes, and Surveys. - Updated handling of priority for In-App Automation, Scenes, and Surveys. Priority is now taken into consideration at each step of displaying a message instead of just sorting messages that are triggered at the same time. - Updated handling of long delays for In-App Automation, Scenes, and Surveys. Delays will now be preprocessed up to 30 seconds before it ends before the message is prepared. ## 18.1.6 August 10, 2024 Patch release that fixes in-app experience displays when resuming from a paused state. Apps that use in-app experiences are encouraged to update. ### Changes - Fixed Automation Engine updates when pause state changes. ## 18.1.5 August 6, 2024 Patch release that fixes test devices audience check and holdout group experiments displays. ### Changes - Fixed test devices audience check. - Fixed holdout group experiment displays. ## 18.1.4 August 1, 2024 Patch release that includes bug fixes for Embedded Content. ### Changes - Fixed an issue with dismissing Embedded Content after pausing and resuming the app. - Updated the default `PreferenceCenterFragment` to scope the `PreferenceCenterViewModel` to the fragment's view lifecycle. ## 18.1.3 July 30, 2024 Patch release that includes bug fixes for Embedded Content and Preference Center, and accessibility improvements for Message Center. ### Changes - Fixed an issue with container child item measurement in Scenes, when margins were set on the container items. - Fixed a Preference Center bug that could lead to subscription channel chips not being visible when initially displaying a Preference Center. - Fixed dismissing multiple embedded views in the same session. - Fixed an issue with automation trigger state not correctly persisting across sessions. - Message Center accessibility improvements. - Updated the default style for the pull to dismiss view in In-App Message Banners to better match iOS. ## 18.1.2 July 16, 2024 Patch release that includes fixes for Preference Center. ### Changes - Fixed warning message on preference center email entry field. - Fixed country code listing. ## 18.1.1 June 28, 2024 Patch release that includes fixes for Preference Center, Privacy Manager, and Embedded Content. ### Changes - Fixed a Preference Center issue that caused contact subscription toggles to show the incorrect state after being toggled - Fixed test dependency being included in the automation module - Fixed Embedded Content impression event interval - Fixed privacy manager crash when enabling, disabling, or setting an empty set of features - Contact channel listing is now refreshed on foreground and from a background push ## 18.1.0 June 21, 2024 Minor SDK release that fixes a potential crash related to analytics during app init and adds public builders for modifying `InAppMessage` and `AutomationSchedule` objects via extenders set on`LegacyInAppMessaging`. ### Changes - Fixed a potential crash related to analytics during app init - Added builders for modifying `InAppMessage` and `AutomationSchedule` objects via extenders set on `LegacyInAppMessaging` ## 18.0.0 June 14, 2024 Major SDK release with several breaking changes. See the [Migration Guides](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-17-18.md) for more info. ### Changes - The Airship SDK now requires `compileSdk` version 34 (Android 14) or higher. - New Automation module - Check schedule’s start date before executing, to better handle updates to the scheduled start date - Improved image loading for In-App messages, Scenes, and Surveys - Reset GIF animations on visibility change in Scenes and Surveys - Pause Story progress while videos are loading - Concurrent automation processing to reduce latency if more than one automation is triggered at the same time - Embedded Scenes & Survey support - New module `urbanairship-automation-compose` to support embedding a Scene & Survey in compose - Added new compound triggers and IAX event triggers - Ban lists support - Added new `PrivacyManager.Feature.FEATURE_FLAGS` to control access to feature flags - Added support for multiple deferred feature flag resolution - Added contact management support in preference centers - Migrated to non-transitive R classes - Removed `urbanairship-ads-identifier` and `urbanairship-preference` modules Initial alpha release of SDK 18.0.0. This version is not suitable for a production app, but we encourage testing out the new APIs and providing us feedback so we can make changes before the final SDK 18 release. The Airship SDK now requires `compileSdk` version 34 (Android 14) or higher. ### Changes - Improved image loading for In-App messages, Scenes, and Surveys - Reset GIF animations on visibility change in Scenes and Surveys - Pause Story progress while videos are loading - Migrated to non-transitive R classes - Check schedule’s start date before executing, to better handle updates to the scheduled start date - Removed `urbanairship-ads-identifier` and `urbanairship-preference` modules See the [Migration Guide](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-17-18.md) for further details. [View Older Releases](https://github.com/urbanairship/android-library/releases?q=created%3A%3C2024-05-15&expanded=true) # Android SDK Resources > API references and other resources for Android development. ## Platform Support {#platform-support} | Feature | Android | Android TV | Fire OS | |----------------------------------------|---------|------------|---------| | Push Notifications | ✅ | ❌ | ✅ | | Live Updates | ✅ | ❌ | ❌ | | In-App Experiences | ✅ | ✅ ¹ | ✅ ² | | Embedded Content | ✅ | ✅ | ✅ | | Message Center | ✅ | ✅ ³ | ✅ ⁴ | | Preference Center | ✅ | ✅ | ✅ | | Feature Flags | ✅ | ✅ | ✅ | | Analytics | ✅ | ✅ | ✅ | | Contacts | ✅ | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ✅ | ¹ **Android TV In-App Experiences:** Modal, Fullscreen, and Banner message styles are supported. Standard In-App Messages are not supported. ² **Fire OS In-App Experiences:** Standard In-App Messages are supported. In-App Automation is not supported. ³ **Android TV Message Center:** The OpenExternalUrlAction to open URLs in messages will not work, as Android TV does not have a web browser. ⁴ **Fire OS Message Center:** The MessageCenterAction opens the Message Center but does not directly open the message. If a web browser is installed, URLs function as button actions. ## API references - [Android API Docs](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/) ## Github Samples * [Sample Apps](https://github.com/urbanairship/android-sample-apps) ## Source * [Source](https://github.com/urbanairship/android-library) ## Changelog * [Changelog](https://www.airship.com/docs/developer/sdk-integration/android/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [Android license](https://github.com/urbanairship/android-library/blob/main/LICENSE) ### SDK Installation Complete installation and configuration guides for the Airship SDK, including setup, advanced integration, logging, and locale configuration. # Install and Set Up the Android SDK > Learn how to install the Airship SDK on Android, Android TV, and Fire OS. The Airship Android SDK is a modular, Kotlin-first SDK with support for both Jetpack Compose and XML Views. It provides a consistent API for push notifications, in-app messaging, and audience management. For a complete reference of feature support across Android, Android TV, and Fire OS, see [Platform Support](https://www.airship.com/docs/developer/sdk-integration/android/resources/#platform-support). > **Tip:** If you use an AI coding assistant, you can connect it to Airship with Skills and an MCP server. See [Airship AI Tools](https://www.airship.com/docs/developer/ai-tools/ai-tools/). ## Requirements * Minimum Android version supported: `23+` * Compile SDK version: `36+` ## SDK Installation The Airship SDK is split into modules which allow you to choose the push providers and features to be included in your application. | Module | Description | |------------------------------------------|----------------------------------------------------------------------------------| | `urbanairship-adm` | ADM push provider | | `urbanairship-fcm` | FCM push provider | | `urbanairship-hms` | HMS push provider | | `urbanairship-automation` | In-App Automation, In-App Messaging, and Landing pages (XML Views UI) | | `urbanairship-automation-compose` | In-App Automation, In-App Messaging, and Landing pages (Compose UI) | | `urbanairship-message-center` | Message Center (XML Views UI) | | `urbanairship-message-center-compose` | Message Center (Compose UI) | | `urbanairship-preference-center` | Preference Center (XML Views UI) | | `urbanairship-preference-center-compose` | Preference Center (Compose UI) | | `urbanairship-live-update` | Live Updates | | `urbanairship-feature-flag` | Feature Flags | Choose the UI framework that matches your app: use `-compose` modules if using Jetpack Compose, otherwise use the standard modules. Do not include both modules in the same app. #### Gradle Kotlin **app build.gradle.kts** ```kotlin dependencies { val airshipVersion = "androidSdkVersion" // FCM push provider implementation("com.urbanairship.android:urbanairship-fcm:$airshipVersion") // In-App Messaging implementation("com.urbanairship.android:urbanairship-automation-compose:$airshipVersion") // Message Center implementation("com.urbanairship.android:urbanairship-message-center-compose:$airshipVersion") } ``` > **Note:** All Airship dependencies included in the `build.gradle.kts` file should specify the exact same version. #### Gradle Groovy **app build.gradle** ```groovy dependencies { def airshipVersion = "androidSdkVersion" // FCM push provider implementation "com.urbanairship.android:urbanairship-fcm:$airshipVersion" // In-App Messaging implementation "com.urbanairship.android:urbanairship-automation:$airshipVersion" // Message Center implementation "com.urbanairship.android:urbanairship-message-center:$airshipVersion" } ``` > **Note:** All Airship dependencies included in the `build.gradle` file should specify the exact same version. ## Initialize Airship The Airship SDK must be initialized before any Receiver, Service, or Activity is created. The recommended way to achieve this is by using the [Autopilot](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-autopilot/index.html) class, but it is also possible to call *takeOff* manually in **Application.onCreate**. ### Setup Autopilot The Airship SDK will automatically launch and load an Autopilot class that can be used to provide custom Airship config and to customize the Airship instance. Autopilot is the recommended approach to integrating the Airship SDK. To start, create a new class that extends `Autopilot`. This class needs to be public and have a default no argument constructor. #### Android Kotlin ```kotlin class SampleAutopilot : Autopilot() { } ``` #### Android Java ```java public class SampleAutopilot extends Autopilot { } ``` Add metadata within the `application` entry to the `AndroidManifest.xml`. The name of the meta-data `com.urbanairship.autopilot` and the value should be the fully qualified class name. **Register the extended Autopilot class** ```xml ``` ### Configuring Airship Airship requires your project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret)to authenticate your application. To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. The [AirshipConfigOptions](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship-config-options/index.html) provides the app credentials and common settings for Airship. To provide an `AirshipConfigOptions` instance, override the method `createAirshipConfigOptions` in your autopilot class. #### Android Kotlin ```kotlin class SampleAutopilot : Autopilot() { override fun createAirshipConfigOptions(context: Context) = airshipConfigOptions { // Set default credentials. Alternatively, you can set production and development separately. setAppKey("YOUR_DEFAULT_APP_KEY") setAppSecret("YOUR_DEFAULT_APP_SECRET") // Set site. (Either Site.SITE_US or Site.SITE_EU) setSite(Site.SITE_US) // Notification config setNotificationAccentColor(context.getColor(R.color.accent)) setNotificationIcon(R.drawable.ic_notification) setNotificationChannel(NotificationProvider.DEFAULT_NOTIFICATION_CHANNEL) } override fun onAirshipReady(context: Context) { // Airship is ready! Configure additional settings here. Airship.push.userNotificationsEnabled = true } } ``` #### Android Java ```java public class SampleAutopilot extends Autopilot { @Override public @Nullable AirshipConfigOptions createAirshipConfigOptions(@NotNull Context context) { return AirshipConfigOptions.newBuilder() // Set default credentials. Alternatively, you can set production and development separately. .setAppKey("YOUR_DEFAULT_APP_KEY") .setAppSecret("YOUR_DEFAULT_APP_SECRET") // Set site. (Either SITE_US or SITE_EU) .setSite(Site.SITE_US) // Notification config .setNotificationAccentColor(context.getColor(R.color.accent)) .setNotificationIcon(R.drawable.ic_notification) .setNotificationChannel(NotificationProvider.DEFAULT_NOTIFICATION_CHANNEL) .build(); } @Override public void onAirshipReady(@NotNull Context context) { // Airship is ready! Configure additional settings here. Airship.getPush().setUserNotificationsEnabled(true); } } ``` > **Note:** Airship config defaults to the US cloud site (`Site.SITE_US`). If your application is set up for the EU site, you must set the site on the config options to `Site.SITE_EU`. ### Customizing Airship behavior {#customizing-airship} Airship provides common config options in `AirshipConfig`, however some more advanced configuration must be set directly on the Airship instance. Custom push handling, deep linking, etc., should be configured during `onAirshipReady` callback. This will ensure Airship is properly configured before handling any messages. The `onAirshipReady` callback will be called on a background thread during Airship init process. Applications should not do any long-running work during this callback, or it might prevent Airship from being ready in time to process a push notification. #### Android Kotlin ```kotlin class SampleAutopilot : Autopilot() { // ... override fun onAirshipReady(context: Context) { // Custom Message Center MessageCenter.shared().setOnShowMessageCenterListener { messageId: String? -> true } // Notification handling Airship.push.addPushListener(MyPushListener()) Airship.push.notificationListener = MyNotificationListener() // etc... } } ``` #### Android Java ```java public class SampleAutopilot extends Autopilot { // ... @Override public void onAirshipReady(@NonNull Context context) { MessageCenter.shared().setOnShowMessageCenterListener(messageId -> { return true; }); Airship.getPush().addPushListener(new MyPushListener()); Airship.getPush().setNotificationListener(new MyNotificationListener()); } } ``` ## Test the integration After completing the setup, verify your integration: 1. **Build and run your app** on your Android device or emulator. 2. **Check the logcat output** for Airship channel creation: - Look for a log message similar to: `Airship channel created: ` - The channel ID will appear in the log output, confirming successful initialization. - For more detailed logging, see [Logging](https://www.airship.com/docs/developer/sdk-integration/android/installation/logging/). If you see the channel ID in the logcat and no errors, your integration is successful. You can now proceed with configuring [deep links](https://www.airship.com/docs/developer/sdk-integration/android/deep-links/), [push notifications](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/getting-started/), and other Airship features. If you don't see a channel ID in the logcat or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/android/troubleshooting/initialization/) for common problems and solutions. # Advanced Integration > Additional configuration for specific use-cases. ## Configuring Airship via properties file If no config is returned from `Autopilot.createAirshipConfigOptions`, Airship will default to loading config from the `airshipconfig.properties` file, located in your application's `assets` directory. This can be useful for in Apps with multiple build variants, or for keeping credentials out of version control. Sample `assets/airshipconfig.properties` file: ```properties # App credentials app_key = YOUR_DEFAULT_APP_KEY app_secret = YOUR_DEFAULT_APP_SECRET # Optionally, set separate production credentials # production_app_key = YOUR_PRODUCTION_APP_KEY # production_app_secret = YOUR_PRODUCTION_APP_SECRET # development_app_key = YOUR_DEVELOPMENT_APP_KEY # development_app_secret = YOUR_DEVELOPMENT_APP_SECRET # Set the cloud site (either SITE_US or SITE_EU) site = SITE_US # In production (true/false) in_production = false # Enable Airship debug logging (true/false) is_airship_debug_enabled = true # Notification configuration notification_icon = @drawable/ic_notification notification_accent_color = @color/accent notification_channel = default_channel ``` The keys used in the `airshipconfig.properties` file match the field names in [AirshipConfigOptions](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship-config-options/index.html) , converted to snake-case. ## URL allowlist The [UrlAllowList](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-url-allow-list/index.html) controls which URLs the Airship SDK is able to act on. The SDK divides up usages of URLs into two different scopes: - `SCOPE_OPEN_URL`: Only URLs allowed for this scope can be opened from an action, displayed in landing page, or displayed in an HTML in-app message. Defaults to allowing all URLs if not specified in the config. - `SCOPE_JAVASCRIPT_INTERFACE`: These URLs are checked before the Airship JavaScript interface is injected into the webview. Defaults to any Airship originated URLs. Allowed URLs should be provided when configuring the Airship Config options. **Valid URL pattern syntax** ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` ## Custom Firebase applications By default, the Airship SDK will use the data in `google-services.json` to configure Firebase Messaging. If your app makes use of multiple Firebase projects, you can instruct the Airship SDK to use a specific named Firebase project for Firebase Cloud Messaging (FCM). In order to create a secondary Firebase application instance, you'll need to manually configure `FirebaseOptions` and initialize your secondary Firebase application. The Firebase application should be initialized before `takeOff`, or during the `onAirshipReady` callback. #### Android Kotlin ```kotlin // Manually configure FirebaseOptions for the secondary Firebase Application. val options = FirebaseOptions.Builder() .setProjectId("Your Firebase Project ID") .setApplicationId("Your Firebase Application ID") .setApiKey("Your Firebase API key") .setGcmSenderId("Your GCM Sender ID") .build() // Initialize the secondary Firebase Application. Firebase.initialize(context, options, "secondary") ``` #### Android Java ```java // Manually configure FirebaseOptions for the secondary Firebase Application. FirebaseOptions options = new FirebaseOptions.Builder() .setProjectId("Your Firebase Project ID") .setApplicationId("Your Firebase Application ID") .setApiKey("Your Firebase API key") .setGcmSenderId("Your GCM Sender ID") .build(); // Initialize the secondary Firebase Application. FirebaseApp.initializeApp(context, options, "secondary"); ``` Now that you have initialized your secondary Firebase application, you can configure the Airship SDK to use it by setting [Firebase app name](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship-config-options/-builder/set-fcm-firebase-app-name.html) name in the `AirshipConfigOptions` instance. ## Extending the FirebaseMessagingService If your application uses its own `FirebaseMessagingService` or some other third party push provider, you will also need to forward `onNewToken` and `onMessageReceived` calls to the Airship SDK. #### Android Kotlin ```kotlin fun onNewToken(token: String) { AirshipFirebaseIntegration.processNewToken(getApplicationContext(), token) } fun onMessageReceived(remoteMessage: RemoteMessage) { AirshipFirebaseIntegration.processMessageSync(getApplicationContext(), message) } ``` #### Android Java ```java @Override public void onNewToken(String token) { AirshipFirebaseIntegration.processNewToken(getApplicationContext(), token); } @Override public void onMessageReceived(RemoteMessage message) { AirshipFirebaseIntegration.processMessageSync(getApplicationContext(), message); } ``` ## Extending the HmsMessageService If your application uses its own `HmsMessageService` or some other third party push provider, you will also need to forward `onNewToken` and `onMessageReceived` calls to the Airship SDK. #### Android Kotlin ```kotlin fun onNewToken(token: String?) { AirshipHmsIntegration.processNewToken(getApplicationContext(), token) } fun onMessageReceived(remoteMessage: RemoteMessage) { AirshipHmsIntegration.processMessageSync(getApplicationContext(), message) } ``` #### Android Java ```java @Override public void onNewToken(@Nullable String token) { AirshipHmsIntegration.processNewToken(getApplicationContext(), token); } @Override public void onMessageReceived(RemoteMessage message) { AirshipHmsIntegration.processMessageSync(getApplicationContext(), message); } ``` # Logging > Configure log levels, privacy settings, and custom log handlers to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. If you don't configure logging, the SDK uses **Info** for development builds and **Error** for production builds with **private** privacy level. ## Log levels The log level acts as a minimum threshold—only logs at that level and higher will be logged. Available log levels, ordered from most to least verbose: | Log Level | Prefix | Description | | :-------- | :----- | :---------- | | **Verbose** | `V` | Highly detailed SDK status for deep debugging and troubleshooting | | **Debug** | `D` | General SDK status with more detailed information than Info | | **Info** | `I` | General SDK status and lifecycle events | | **Warning** | `W` | API deprecations, invalid setup, and other recoverable issues | | **Error** | `E` | Critical errors and exceptions that the SDK cannot gracefully handle | | **Assert** | — | Disables all logging | ## Log privacy levels Control the visibility of log contents using privacy levels. This is especially useful when debugging release builds without exposing sensitive information. - **private** (default): Uses the standard `android.util.Log`. In production builds, `verbose` and `debug` messages are completely dropped and will not be logged. Use this for production builds to protect sensitive data. - **public**: Sends all logs with a `public` privacy level, making it easier to capture detailed information from release builds. To ensure visibility in production builds, `verbose` and `debug` messages are automatically elevated to the `info` log level. > **Note:** When using `public` privacy level, `verbose` and `debug` log messages are automatically elevated to `info` level to ensure all detailed logs are visible when debugging production builds. ## Configuration You can set separate log levels and privacy levels for development and production builds in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#calling-takeoff). ### Common configuration Typical setup: more verbose logging for development, minimal logging for production: #### Kotlin ```kotlin airshipConfigOptions { // Development: verbose logging for debugging setDevelopmentLogLevel(AirshipConfigOptions.LogLevel.VERBOSE) setDevelopmentLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PUBLIC) // Production: minimal logging to reduce noise setProductionLogLevel(AirshipConfigOptions.LogLevel.ERROR) setProductionLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PRIVATE) // ... } ``` #### Java ```java AirshipConfigOptions.newBuilder() // Development: verbose logging for debugging .setDevelopmentLogLevel(AirshipConfigOptions.LogLevel.VERBOSE) .setDevelopmentLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PUBLIC) // Production: minimal logging to reduce noise .setProductionLogLevel(AirshipConfigOptions.LogLevel.ERROR) .setProductionLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PRIVATE) ... ``` ### Debugging production issues When debugging issues in production builds, temporarily enable verbose logging to capture detailed SDK behavior: #### Kotlin ```kotlin airshipConfigOptions { // Production debugging: enable verbose logs setProductionLogLevel(AirshipConfigOptions.LogLevel.VERBOSE) setProductionLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PUBLIC) // ... } ``` #### Java ```java AirshipConfigOptions.newBuilder() // Production debugging: enable verbose logs .setProductionLogLevel(AirshipConfigOptions.LogLevel.VERBOSE) .setProductionLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PUBLIC) ... ``` ## Verifying log level You can confirm the current log level by checking the Airship log output in your console. On Android, Airship logs use the standard log priority tags (e.g., `V`, `D`, `I`). To find them, filter your logs for the tag **`UAirship`** and observe the priority letter. **Example (Verbose Log)** The `V` in the output indicates a `VERBOSE` level log. ```text Sample - UALib com.urbanairship.sample V UAirship - !SDK-VERSION-STRING!:com.urbanairship.android:urbanairship-core:16.9.0 ``` ## Custom log handler You can provide a custom log handler to intercept and handle all Airship log messages. This is useful when you need to integrate Airship logs with your own logging system or customize how logs are formatted or stored. When a custom log handler is set, the default Airship log handler is completely replaced. Log level filtering is performed before your handler is called, so your handler will only receive logs that meet the configured log level threshold. Implement the `AirshipLogHandler` interface and set it on `UALog.logHandler` before calling `Airship.takeOff()`: #### Kotlin ```kotlin // Example log handler to forward logs to Android logcat and upload to a remote logging service val customLogHandler = AirshipLogHandler { tag, logLevel, throwable, message -> val msg = message() // Forward to Android logcat when (logLevel) { Log.VERBOSE -> Log.v(tag, msg, throwable) Log.DEBUG -> Log.d(tag, msg, throwable) Log.INFO -> Log.i(tag, msg, throwable) Log.WARN -> Log.w(tag, msg, throwable) Log.ERROR -> Log.e(tag, msg, throwable) else -> Unit // Do nothing } // Optionally: send to remote logging service MyLoggingService.log(tag, logLevel, msg, throwable) } // Set the custom handler globally, before Airship.takeOff() UALog.logHandler = customLogHandler ``` #### Java ```java AirshipLogHandler logHandler = new AirshipLogHandler() { @Override public void log(@NotNull String tag, int logLevel, @Nullable Throwable throwable, @NotNull Function0<@NotNull String> message) { String msg = message.invoke(); // Forward to Android logcat switch (logLevel) { case Log.VERBOSE: Log.v(tag, msg, throwable); break; case Log.DEBUG: Log.d(tag, msg, throwable); break; case Log.INFO: Log.i(tag, msg, throwable); break; case Log.WARN: Log.w(tag, msg, throwable); break; case Log.ERROR: Log.e(tag, msg, throwable); break; default: break; // Do nothing } // Optionally: send to remote logging service MyLoggingService.log(tag, logLevel, msg, throwable); } }; // Set the custom handler globally, before Airship.takeOff() UALog.setLogHandler(logHandler); ``` # Locale > Configure locale behavior and override the default locale that Airship uses. Airship uses the [Locale](https://www.airship.com/docs/reference/glossary/#locale) for various SDK operations. By default, the SDK automatically uses the device's locale settings, but you can configure it to use the user's preferred language or override it programmatically. ## Overriding the locale You can override the locale programmatically at runtime, which takes precedence over the device's locale settings. #### Kotlin ```kotlin Airship.localeManager.setLocaleOverride(Locale.GERMANY) ``` #### Java ```java Airship.getLocaleManager().setLocaleOverride(new Locale("de")); ``` ## Clearing the locale override To remove a locale override and return to using the device's locale settings: #### Kotlin ```kotlin Airship.localeManager.setLocaleOverride(null) ``` #### Java ```java Airship.getLocaleManager().setLocaleOverride(null); ``` ## Getting the current locale To retrieve the locale that Airship is currently using: #### Kotlin ```kotlin val airshipLocale = Airship.localeManager.locale ``` #### Java ```java Locale airshipLocale = Airship.getLocaleManager().getLocale(); ``` ### Push Notifications Comprehensive guides for implementing push notifications, including setup, notification channels, and advanced customizations. # Push Notifications > How to configure your application to receive and respond to notifications. ## Push Provider Setup Configure a push provider to enable push notifications on Android devices. ### FCM 1. Follow [FCM Android Setup](https://firebase.google.com/docs/android/setup) to configure your Android application to connect to Firebase. 1. Add the `urbanairship-fcm` dependency to your application's build.gradle file: #### Gradle Kotlin ```kotlin implementation("com.urbanairship.android:urbanairship-fcm:$airshipVersion") ``` #### Gradle Groovy ```groovy implementation "com.urbanairship.android:urbanairship-fcm:$airshipVersion" ``` ### ADM 1. Follow [Amazon's documentation](https://developer.amazon.com/docs/adm/integrate-your-app.html#store-your-api-key-as-an-asset) to store your API key as an asset. 1. Add the `urbanairship-adm` dependency to your application's build.gradle file: #### Gradle Kotlin ```kotlin implementation("com.urbanairship.android:urbanairship-adm:$airshipVersion") ``` #### Gradle Groovy ```groovy implementation "com.urbanairship.android:urbanairship-adm:$airshipVersion" ``` ### HMS 1. Follow [Huawei's documentation](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-integrating-sdk-0000001050040084) to set up the HMS SDK. > **Note:** Airship requires HMS Core Push SDK 6.3.0.304 or newer. 1. Add the `urbanairship-hms` dependency to your application's build.gradle file: #### Gradle Kotlin ```kotlin implementation("com.urbanairship.android:urbanairship-hms:$airshipVersion") ``` #### Gradle Groovy ```groovy implementation "com.urbanairship.android:urbanairship-hms:$airshipVersion" ``` ## Enable User Notifications Enabling `userNotificationsEnabled` will prompt the user for permission to send notifications. To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately, and instead wait for a more appropriate time in the app. The Airship SDK makes a distinction between `user notifications`, which can be seen by the user, and other forms of push that allow you to send data to your app silently, or in the background. Enabling or disabling user notifications is a preference often best left up to the user, so by default, user notifications are disabled. #### Kotlin ```kotlin Airship.push.userNotificationsEnabled = true ``` #### Java ```java Airship.getPush().setUserNotificationsEnabled(true); ``` > **Note:** For apps that target Android 13 (API 33) and above, enabling user notifications will display a runtime permission prompt to allow notifications to be sent. > > To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately on app startup, and instead wait for a more appropriate time to prompt for notification permission. ## Handle Notification Events The Airship SDK provides several callbacks for when a push is received or a notification is interacted with. Apps can use these callbacks to do custom push processing. Registering for a callback is optional, the SDK will automatically launch the application without the need to set a callback. #### Kotlin ```kotlin Airship.push.addPushListener { message: PushMessage, notificationPosted: Boolean -> // Called when a message is received } Airship.push.notificationListener = object : NotificationListener { override fun onNotificationPosted(notificationInfo: NotificationInfo) { // Called when a notification is posted } override fun onNotificationOpened(notificationInfo: NotificationInfo): Boolean { // Called when a notification is tapped. // Return false here to allow Airship to auto launch the launcher activity return false } override fun onNotificationForegroundAction( notificationInfo: NotificationInfo, actionButtonInfo: NotificationActionButtonInfo ): Boolean { // Called when a notification action button is tapped. // Return false here to allow Airship to auto launch the launcher activity return false } override fun onNotificationBackgroundAction( notificationInfo: NotificationInfo, actionButtonInfo: NotificationActionButtonInfo ) { // Called when a background notification action button is tapped. } override fun onNotificationDismissed(notificationInfo: NotificationInfo) { // Called when a notification is dismissed } } ``` #### Java ```java Airship.getPush().addPushListener((message, notificationPosted) -> { // Called when any push is received }); Airship.getPush().setNotificationListener(new NotificationListener() { @Override public void onNotificationPosted(@NonNull NotificationInfo notificationInfo) { // Called when a notification is posted } @Override public boolean onNotificationOpened(@NonNull NotificationInfo notificationInfo) { // Called when a notification is tapped. // Return false here to allow Airship to auto launch the launcher activity return false; } @Override public boolean onNotificationForegroundAction(@NonNull NotificationInfo notificationInfo, @NonNull NotificationActionButtonInfo actionButtonInfo) { // Called when a notification action button is tapped. // Return false here to allow Airship to auto launch the launcher activity return false; } @Override public void onNotificationBackgroundAction(@NonNull NotificationInfo notificationInfo, @NonNull NotificationActionButtonInfo actionButtonInfo) { // Called when a background notification action button is tapped. } @Override public void onNotificationDismissed(@NonNull NotificationInfo notificationInfo) { // Called when a notification is dismissed } }); ``` ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. For Android, all push messages are delivered in the background, but default Airship will treat messages without an `alert` as silent. > **Note:** Pushes sent without an `alert` property do not have guaranteed delivery. Factors affecting delivery include battery life, whether the device is connected to WiFi, and the number of silent pushes sent within a recent time period. These metrics are determined solely by Android and FCM. Therefore, this feature is best used for supplementing the regular behavior of the app rather than providing critical functionality. For instance, an app could use a silent push to pre-fetch new data ahead of time in order to reduce load times when the app is later launched by the user. ## Next Steps - [Notification Channels](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/advanced-customizations/#notification-channels) - Create custom notification channels for Android - [Custom Notification Provider](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/advanced-customizations/#custom-notification-provider) - Customize how notifications are displayed - [Interactive Notifications](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/advanced-customizations/#interactive-notifications) - Add action buttons to notifications If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/android/troubleshooting/push-notifications/) to check notification status and fix common issues. # Advanced Customizations > Customize notification appearance, behavior, and interactions with Android-specific features. ## Notification channels Starting with Android O, each notification must assign a valid notification channel or the notification will not display. The SDK will automatically assign a default channel with the name "Notifications". The default channel can be overridden either in [AirshipConfigOptions](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship-config-options/-builder/set-notification-channel.html) or the notification channel can also be set per message by specifying the channel's ID in the [Push API](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#androidoverrideobject). ### Compat notification channels Notification channel compat allows you to define notification channels for all Android versions. For pre-O Android devices, the Airship SDK will apply the notification channel settings on the notification before posting. This allows an app to define channels and use them across all devices. #### Kotlin ```kotlin override fun onAirshipReady(context: Context) { // ... val channelCompat = NotificationChannelCompat( "customChannel", "Breaking News!", NotificationManagerCompat.IMPORTANCE_DEFAULT ) Airship.push .notificationChannelRegistry .createNotificationChannel(channelCompat) } ``` #### Java ```java @Override public void onAirshipReady(@NonNull Context context) { // ... NotificationChannelCompat channelCompat = new NotificationChannelCompat( "customChannel", "Breaking News!", NotificationManagerCompat.IMPORTANCE_DEFAULT); Airship.getPush() .getNotificationChannelRegistry() .createNotificationChannel(channelCompat); } ``` > **Note:** The Airship SDK will fall back to the default notification channel if you set a notification channel ID that doesn't exist, so make sure that you created one with the same ID. ## Clearing notifications Notifications can be cleared manually by using standard Android APIs on the [NotificationManager](https://developer.android.com/reference/android/app/NotificationManager.html) or [NotificationManagerCompat](https://developer.android.com/reference/android/support/v4/app/NotificationManagerCompat.html) classes. #### Kotlin ```kotlin NotificationManagerCompat.from(context).cancelAll() ``` #### Java ```java NotificationManagerCompat.from(context).cancelAll(); ``` ## Control foreground notification display By default, notifications are displayed even when the app is in the foreground. To override that per message, set `foregroundNotificationDisplayPredicate` on `Airship.push`. The predicate runs for every incoming push; return `true` to present the notification or `false` to suppress it. Set the predicate to `null` to restore the default behavior. #### Kotlin ```kotlin Airship.push.foregroundNotificationDisplayPredicate = Predicate { message -> // Example: only show when getExtra("display_in_foreground") == "true" message.getExtra("display_in_foreground") == "true" } ``` #### Java ```java Airship.getPush().setForegroundNotificationDisplayPredicate(message -> { // Example: only show when getExtra("display_in_foreground") == "true" return "true".equals(message.getExtra("display_in_foreground")); }); ``` ## Custom notification provider The `AirshipNotificationProvider` is the recommended factory as it provides full support for all of the [Android push features](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#androidoverrideobject). All incoming push notifications are built using a class that implements the [NotificationProvider](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-notification-provider/index.html) . The Airship SDK uses the [AirshipNotificationProvider](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-airship-notification-provider/index.html) . With this provider, the standard Android Notification layout will use the application's title and icon. A default big text style will be applied for all notifications. ![Default notification layout using AirshipNotificationProvider](https://www.airship.com/docs/images/default-factory-notification_hu_828cc11f66780a38.webp) *Default notification layout using AirshipNotificationProvider* ## Custom notification factory Custom notification factories are supported, but may cause some [Android Push features](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#androidoverrideobject) to no longer work. Only features that the custom factory implements will be available. #### Kotlin ```kotlin class CustomNotificationFactory : NotificationProvider { @WorkerThread override fun onCreateNotificationArguments(context: Context, message: PushMessage): NotificationArguments { val channel = requireNotNull(message.getNotificationChannel("defaultChannel")) return NotificationArguments.newBuilder(message) .setNotificationChannelId(channel) .setNotificationId(message.notificationTag, NotificationIdGenerator.nextID()) .build() } @WorkerThread override fun onCreateNotification(context: Context, arguments: NotificationArguments): NotificationResult { val message = arguments.message // do not display a notification if there is not an alert if (message.alert.isNullOrEmpty()) { return NotificationResult.cancel() } // Build the notification val builder = NotificationCompat.Builder(context) .setContentTitle("Notification title") .setContentText(message.alert) .setAutoCancel(true) .setSmallIcon(R.drawable.notification_icon) // Notification action buttons builder.extend(ActionsNotificationExtender(context, message, notificationId)) return NotificationResult.notification(builder.build()) } @WorkerThread override fun onNotificationCreated(context: Context, notification: Notification, arguments: NotificationArguments) { // Called after the notification is built, right before posting the notifications. Apply any global // defaults to the notification } } ``` #### Java ```java public class CustomNotificationFactory implements NotificationProvider { @WorkerThread @NonNull @Override public NotificationArguments onCreateNotificationArguments(@NonNull Context context, @NonNull PushMessage message) { String channel = message.getNotificationChannel("defaultChannel"); return NotificationArguments.newBuilder(message) .setNotificationChannelId(channel) .setNotificationId(message.getNotificationTag(), NotificationIdGenerator.nextID()) .build(); } @WorkerThread @NonNull @Override public NotificationResult onCreateNotification(@NonNull Context context, @NonNull NotificationArguments arguments) { PushMessage message = arguments.getMessage(); // do not display a notification if there is not an alert if (UAStringUtil.isEmpty(message.getAlert())) { return NotificationResult.cancel(); } // Build the notification NotificationCompat.Builder builder = new NotificationCompat.Builder(context) .setContentTitle("Notification title") .setContentText(message.getAlert()) .setAutoCancel(true) // Make sure that your icon follows Google's Guidelines : a white icon with transparent background .setSmallIcon(R.drawable.notification_icon); // Notification action buttons builder.extend(new ActionsNotificationExtender(context, message, notificationId)); return NotificationResult.notification(builder.build); } @WorkerThread public void onNotificationCreated(@NonNull Context context, @NonNull Notification notification, @NonNull NotificationArguments arguments) { // Called after the notification is built, right before posting the notifications. Apply any global // defaults to the notification } } ``` For simple modifications, it is recommended to extend the `AirshipNotificationProvider` instead to ensure all Airship push features continue to work. #### Kotlin ```kotlin class CustomNotificationFactory(context: Context, configOptions: AirshipConfigOptions) : AirshipNotificationProvider(context, configOptions) { @WorkerThread override fun onExtendBuilder( context: Context, builder: NotificationCompat.Builder, arguments: NotificationArguments ): NotificationCompat.Builder { val newBuilder = super.onExtendBuilder(context, builder, arguments) // Apply any defaults to the builder return newBuilder } } ``` #### Java ```java public class CustomNotificationFactory extends AirshipNotificationProvider { public CustomNotificationFactory( @NonNull Context context, @NonNull AirshipConfigOptions configOptions ) { super(context, configOptions); } @WorkerThread @NonNull @Override protected NotificationCompat.Builder onExtendBuilder( @NonNull Context context, @NonNull NotificationCompat.Builder builder, @NonNull NotificationArguments arguments ) { builder = super.onExtendBuilder(context, builder, arguments); // Apply any defaults to the builder return builder; } } ``` #### Kotlin ```kotlin override fun onAirshipReady(context: Context) { Airship.push.notificationProvider = CustomDefaultNotificationProvider() } ``` #### Java ```java @Override public void onAirshipReady(Context context) { Airship.getPush() .setNotificationProvider(new CustomDefaultNotificationProvider()); } ``` ## Available notification extenders The SDK also provides several notification builder extenders to help support [Android Push features](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#androidoverrideobject). [ActionsNotificationExtender](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-actions-notification-extender/index.html) : Supports Android Notification Action Button API features. [PublicNotificationExtender](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-public-notification-extender/index.html) : Supports Public Notification API features. [StyleNotificationExtender](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-style-notification-extender/index.html) : Supports Android style API features. [WearableNotificationExtender](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-wearable-notification-extender/index.html) : Supports Android Wear API features. ## Interactive notifications You can add action buttons to notifications to allow users to interact without opening the app. ### Standard interactive notifications Airship provides a set of standard Interactive Notification types (See: [Built-In Interactive Notification Types](https://www.airship.com/docs/reference/messages/built-in-interactive-notifications/)). It is the *type* that determines which buttons and corresponding labels will be available when you send a push. See the next section for where to specify that in the push payload. You control what happens when you send the push separately, by tying each button ID to a specific action. ### Custom interactive notification types (notification action button groups) If you want to define a custom Interactive Notification type, you must register a new notification action button group. > **Note:** Airship reserves category IDs prefixed with `ua_`. Any custom groups with that prefix will be dropped. Custom [NotificationActionButtonGroups](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-notification-action-button-group/index.html) are supported by registering the groups with the [PushManager](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push/-push-manager/index.html) right after [UAirship.takeOff](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship/take-off.html) using the [PushManager.addNotificationActionButtonGroup](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push/-push-manager/add-notification-action-button-group.html) method. #### Kotlin ```kotlin // Define actions for the group val hiButtonAction: NotificationActionButton = NotificationActionButton.Builder("hi") .setLabel(R.string.hi) .setIcon(R.drawable.your_icon_file) .setPerformsInForeground(true) .build() val byeButtonAction: NotificationActionButton = NotificationActionButton.Builder("bye") .setLabel(R.string.bye) .setIcon(R.drawable.your_icon_file) .setPerformsInForeground(true) .build() // Define the group val buttonGroup = NotificationActionButtonGroup.Builder() .addNotificationActionButton(hiButtonAction) .addNotificationActionButton(byeButtonAction) .build() // Add the custom group Airship.push.pushManager.addNotificationActionButtonGroup("custom group", buttonGroup) ``` #### Java ```java // Define actions for the group NotificationActionButton hiButtonAction = new NotificationActionButton.Builder("hi") .setLabel(R.string.hi) .setIcon(R.drawable.your_icon_file) .setPerformsInForeground(true) .build(); NotificationActionButton byeButtonAction = new NotificationActionButton.Builder("bye") .setLabel(R.string.bye) .setIcon(R.drawable.your_icon_file) .setPerformsInForeground(true) .build(); // Define the group NotificationActionButtonGroup buttonGroup = new NotificationActionButtonGroup.Builder() .addNotificationActionButton(hiButtonAction) .addNotificationActionButton(byeButtonAction) .build(); // Add the custom group Airship.getPushManager().addNotificationActionButtonGroup("custom group", buttonGroup); ``` ### In-App Experiences Implement Scenes, In-App Automations, custom views, and embedded content to deliver engaging in-app experiences to your users. # In-App Experiences > Integrate Scenes & In-App Automations into your Android app to display embedded content and create custom in-app experiences with minimal code. In-App Experiences use Airship's on-device automation framework to provide instant, personalized content that integrates natively with your app. This includes [Scenes](https://www.airship.com/docs/reference/glossary/#scene), which can be displayed as modal or fullscreen overlays or embedded directly within your app screens, and In-App Automations (IAA), which power banner, modal, and fullscreen in-app messages triggered by events. Scenes are fully customizable in the Airship dashboard and require minimal SDK integration. For advanced In-App Automation customization options, see [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/in-app-automation/). ## Requirements To use In-App Experiences, you need: - The `urbanairship-automation` module installed (see [Getting Started](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/)) - Airship SDK initialized (see [Getting Started](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/)) > **Note:** **In-App Experiences work out of the box**: Once you install the `urbanairship-automation` module and initialize Airship, In-App Experiences will function automatically. The rest of this documentation covers optional customization and advanced features. ## Controlling display Control when and how In-App Experiences are displayed in your app. You can auto-pause displays on launch (useful for splash screens), manually pause all in-app displays, set intervals between displays, and control when individual messages are ready to display. ### Auto-pausing on launch For apps with splash screens, you can configure Airship to automatically pause In-App Automation on launch. This prevents In-App Experiences from displaying during the splash screen. Once your app is ready, resume display by setting `isPaused` to `false`. Set the `autoPauseInAppAutomationOnLaunch` option in your Airship config when building `AirshipConfigOptions` (in your Autopilot's `createAirshipConfigOptions` or in `airshipconfig.properties` as `auto_pause_in_app_automation_on_launch = true`): #### Kotlin ```kotlin override fun createAirshipConfigOptions(context: Context) = airshipConfigOptions { // ... other config settings ... // Auto-pause on launch for splash screen setAutoPauseInAppAutomationOnLaunch(true) } ``` When your splash screen is dismissed and the app is ready, resume display: ```kotlin InAppAutomation.shared().isPaused = false ``` #### Java ```java @Override public AirshipConfigOptions createAirshipConfigOptions(Context context) { return AirshipConfigOptions.newBuilder() // ... other config settings ... // Auto-pause on launch for splash screen .setAutoPauseInAppAutomationOnLaunch(true) .build(); } ``` When your splash screen is dismissed and the app is ready, resume display: ```java InAppAutomation.shared().setPaused(false); ``` ### Pausing display Pausing will still allow In-App Experiences to be triggered and queued up for execution, but they will not display. This is useful for preventing in-app experiences from displaying on screens where it would be detrimental to the user experience, such as splash screens, settings screens, or landing pages. #### Kotlin ```kotlin InAppAutomation.shared().isPaused = true ``` #### Java ```java InAppAutomation.shared().setPaused(true); ``` ### Display interval The display interval controls the amount of time to wait before the manager can display the next triggered In-App Experience. The default value is set to **0 seconds** and can be adjusted to any amount of time in seconds. #### Kotlin ```kotlin InAppAutomation.shared().inAppMessaging.displayInterval = 30 ``` #### Java ```java InAppAutomation.shared().getInAppMessaging().setDisplayInterval(30); ``` ### Controlling per-message display You can control when individual In-App Experiences are ready to display and listen for when they are displayed or finished. This is useful when you need to check app state before displaying content, such as: - Verifying the current Activity is appropriate for the message - Checking custom data in the message's extras (custom keys) to determine if it should display - Ensuring certain app conditions are met before showing the message - Integrating with other in-app messaging products #### Kotlin Set a delegate to control the display. You have access to the message and schedule ID: ```kotlin InAppAutomation.shared().inAppMessaging.displayDelegate = object : InAppMessageDisplayDelegate { override fun isMessageReadyToDisplay(message: InAppMessage, scheduleId: String): Boolean { // Return false to prevent display return false } override fun messageWillDisplay(message: InAppMessage, scheduleId: String) { // Message displayed } override fun messageFinishedDisplaying(message: InAppMessage, scheduleId: String) { // Message finished } } ``` `isMessageReadyToDisplay` will be called whenever state in the app changes (Activity, app state, message finished displaying, etc...), you can also trigger it manually with `notifyDisplayConditionsChanged`: ```kotlin InAppAutomation.shared().inAppMessaging.notifyDisplayConditionsChanged() ``` #### Java Implement the `InAppMessageDisplayDelegate` to control the display. You have access to the message and schedule ID: **Implement the InAppMessageDisplayDelegate** ```java InAppAutomation.shared().getInAppMessaging().setDisplayDelegate(new InAppMessageDisplayDelegate() { @Override public boolean isMessageReadyToDisplay(@NonNull InAppMessage message, @NonNull String scheduleId) { // Return false to prevent display return false; } @Override public void messageWillDisplay(@NonNull InAppMessage message, @NonNull String scheduleId) { // Message displayed } @Override public void messageFinishedDisplaying(@NonNull InAppMessage message, @NonNull String scheduleId) { // Message finished } }); ``` `isMessageReadyToDisplay` will be called whenever state in the app changes (Activity, app state, message finished displaying, etc...), you can also trigger it manually with `notifyDisplayConditionsChanged`: ```java InAppAutomation.shared().getInAppMessaging().notifyDisplayConditionsChanged(); ``` ## Next steps - Learn how to [create Scenes in the Airship dashboard](https://www.airship.com/docs/guides/messaging/in-app-experiences/scenes/create/) - Present Scene content with [Embedded Content](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/embedded-content/) - Create reusable components with [Custom Views](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/custom-views/) - Customize [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/in-app-automation/) for IAA # Embedded Content > Integrate Embedded Content into your Android app to display Scene content directly within your app's screens. For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see [Embedded Content](https://www.airship.com/docs/guides/features/messaging/scenes/embedded-content/). You can set up Embedded Content for Android using Jetpack Compose or XML Views. If you are not already on Android SDK 18.1.4+, see the [Airship Android SDK 17.x to 18.0 migration guide](https://github.com/urbanairship/android-library/blob/18.0.0/documentation/migration/migration-guide-17-18.md). ## Jetpack Compose setup Embedded Content support for Jetpack Compose is provided by an extension library, which must be declared as a dependency of your project. #### Gradle Kotlin **app build.gradle.kts** ```kotlin dependencies { val airshipVersion = "androidSdkVersion" // Other Airship dependencies... implementation("com.urbanairship.android:urbanairship-automation-compose:$airshipVersion") } ``` > **Note:** All Airship dependencies included in the `build.gradle.kts` file should all specify the exact same version. #### Gradle Groovy **app build.gradle** ```groovy dependencies { def airshipVersion = "androidSdkVersion" // Other Airship dependencies... implementation "com.urbanairship.android:urbanairship-automation-compose:$airshipVersion" } ``` > **Note:** All Airship dependencies included in the `build.gradle` file should all specify the exact same version. ### Adding an AirshipEmbeddedView The `AirshipEmbeddedView` is a Composable UI element that defines a place for Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `embeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```kotlin import com.urbanairship.automation.compose.AirshipEmbeddedView @Composable fun HomeScreenBanner() { // Show any "home_banner" Embedded Content AirshipEmbeddedView( embeddedId = "home_banner", modifier = Modifier.fillMaxWidth().height(300.dp) ) } ``` ### Placeholders If no content is available to display, the embedded view can optionally show a placeholder. The placeholder can be configured by providing a composable lambda that defines the placeholder content. If no placeholder is set, the embedded view will use the default behavior: - If content is available for the `embeddedId`, the `AirshipEmbeddedView` will display it within your composition. - If no content is available for the `embeddedId`, the `AirshipEmbeddedView` will not be visible. - Compose previews will show a default placeholder that displays the `embeddedId`. **Basic integration with placeholder** ```kotlin AirshipEmbeddedView( embeddedId = "home_banner", modifier = Modifier.fillMaxWidth().wrapContentHeight() ) { Text("Placeholder!", Modifier.align(Alignment.Center)) } ``` ### Placing in a Scrolling Container When placed directly in a scrolling Composable, or in a nested Composable within the scrolling parent that is not bounded in the scroll direction, you must provide the parent's size for the corresponding dimension of the embedded view. This enables percent-based sizing to work correctly. A simple way to accomplish this is to use the `onSizeChanged` modifier to store the size of the scrolling parent (or another ancestor) so that the size can be passed to the embedded view via the `parentWidthProvider` or `parentHeightProvider` arguments. **verticalScroll modifier example** ```kotlin val scrollState = rememberScrollState() var parentHeight by remember { mutableIntStateOf(0) } Column( modifier = Modifier.fillMaxSize() .onSizeChanged { parentHeight = it.height } .verticalScroll(scrollState) ) { AirshipEmbeddedView( embeddedId = "home_banner", parentHeightProvider = { parentHeight }, modifier = Modifier.fillMaxWidth() ) // ... } ``` ### Placing in a Lazy Container An approach similar to the [above method](#placing-in-a-scrolling-container) can be used for sizing embedded views inside of Lazy scrolling containers, such as `LazyColumn` or `LazyRow`. It's important to remember to hoist the embedded view state above the Lazy container so that the embedded view can be recycled and re-created properly. You can do this by calling `rememberAirshipEmbeddedViewState` and passing the embedded view ID as an argument, which returns an embedded view state-holder instance for the given `embeddedId`. In the example below, you'll notice that the `AirshipEmbeddedView` call doesn't include an `embeddedId` argument. This is because the `embeddedId` is provided by the remembered `AirshipEmbeddedViewState` instance. **Lazy scrolling example** ```kotlin val lazyListState = rememberLazyListState() // Hoist the embedded state above the LazyColumn. val embeddedViewState = rememberAirshipEmbeddedViewState(embeddedId = "home_banner") var parentHeight by remember { mutableIntStateOf(0) } LazyColumn( state = lazyListState, modifier = Modifier.fillMaxSize() .onSizeChanged { parentHeight = it.height } ) { item { AirshipEmbeddedView( // The embeddedId of "home_banner" from embeddedViewState // will be used by the embedded view. state = embeddedViewState, parentHeightProvider = { parentHeight }, modifier = Modifier.fillMaxWidth() ) } // ... } ``` ## XML Views setup You can use XML Views instead of [Jetpack Compose](#jetpack-compose-setup). ### Adding an AirshipEmbeddedView The `AirshipEmbeddedView` is an Android `View` that defines a place for Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `airshipEmbeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```xml ``` ### Placeholders If no content is available to display, the embedded view can optionally show a placeholder. The placeholder can be configured by providing a reference to an XML layout that defines the placeholder content. If no placeholder is set, the embedded view will use the default behavior: - If content is available for the `airshipEmbeddedId`, the `AirshipEmbeddedView` will display it within your layout. - If no content is available for the `airshipEmbeddedId`, the `AirshipEmbeddedView` will not be visible. **Basic integration with placeholder** ```xml ``` ### Placing in a ScrollView or RecyclerView When placed directly in a `ScrollView` or `RecyclerView`, or as a nested child view within a scrolling view that is not bounded in the scroll direction, you must provide the parent's size for the corresponding dimension of the embedded view. This enables percent-based sizing to work correctly. You'll need to determine the container size of the scrolling parent (or another ancestor) and pass the size to the embedded view via the `parentWidthProvider` or `ParentHeightProvider` arguments. **ScrollView example** ```kotlin override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val scrollView = findViewById(R.id.scroll_view) val embeddedView = findViewById(R.id.home_banner_embedded_view) scrollView.doOnPreDraw { embeddedView.parentHeightProvider = { scrollView.height } } } ``` Use with `RecyclerView` is similar to the above example, but you'll need to set the parent size in the `onBindViewHolder` method. One way to accomplish this is to pass the parent size to the adapter so that it can be used when binding the view holder that contains the embedded view. ## Controlling content display order By default, pending Embedded Content is displayed in First In, First Out (FIFO) order per `embeddedId`. If you want to control the order in which pending content is displayed, you can provide a custom `Comparator` to sort the Embedded Content based on fields that you define in the content's extras. #### Compose ```kotlin AirshipEmbeddedView( embeddedId = "home_banner", comparator = { a, b -> // Compare based on the priority field set on the Embedded Content extras. val priorityA = a.extras.opt("priority").getInt(0) val priorityB = b.extras.opt("priority").getInt(0) priorityA.compareTo(priorityB) }, modifier = Modifier.fillMaxWidth() ) ``` A `Comparator` can also be passed as an argument to `rememberAirshipEmbeddedViewState` to control content display order when hoisting the embedded view state. #### XML View ```kotlin override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val embeddedView = findViewById(R.id.home_banner_embedded_view) embeddedView.comparator = Comparator { a, b -> // Compare based on the priority field set on the Embedded Content extras. val priorityA = a.extras.opt("priority").getInt(0) val priorityB = b.extras.opt("priority").getInt(0) priorityA.compareTo(priorityB) } } ``` ## Observing available Embedded Content Embedded Content is not always available, and even after being triggered, it still needs to be prepared before it can be displayed. An `AirshipEmbeddedView` will automatically update when content is available and transition from the placeholder to the content once content is available. If you need to query the availability of Embedded Content, you can use an `AirshipEmbeddedObserver` to watch for updates. An `AirshipEmbeddedObserver` exposes both a callback and a `Flow` that can be used to receive updates about the availability of Embedded Content. This allows for more dynamic handling of Embedded Content than just content or a placeholder. #### Kotlin **Flow usage** ```kotlin val observer = AirshipEmbeddedObserver("playground") val embeddedInfo = observer.embeddedViewInfoFlow.collectAsState(initial = emptyList()) if (embeddedInfo.value.isEmpty()) { Text("No banner available") } else { Text("Banner available") AirshipEmbeddedView(embeddedId = "home_banner") } ``` #### Java **Callback usage** ```java AirshipEmbeddedObserver observer = new AirshipEmbeddedObserver("home_banner"); observer.setListener(new AirshipEmbeddedObserver.Listener() { @Override public void onEmbeddedViewInfoUpdate(@NonNull List views) { if (views.isEmpty()) { textView.setText("No banner available"); embeddedView.setVisibility(View.GONE); } else { textView.setText("Banner available"); embeddedView.setVisibility(View.VISIBLE); } } }); ``` The `AirshipEmbeddedObserver` can be created to watch for one or more `embeddedId` values or use custom filtering to watch all Embedded Content or a subset determined by inspecting the `embeddedId` or `extras` associated with the Embedded Content. The `embeddedInfos` returned by the callback or `Flow` are in FIFO order, meaning that the first content in the list is the first content that will be displayed. # Custom Views > Register custom Android views with the AirshipCustomViewManager to use them in Scenes. ![Custom View rendered in a Scene on Android](https://www.airship.com/docs/images/custom-views-android_hu_e02177d308b3e6b5.webp) *Custom View rendered in a Scene on Android* To use Custom Views, you must first register the view's name with the `AirshipCustomViewManager`. The name is referenced when adding the Custom View to a Scene. The view manager will call through to the view builders registered for that view's name and provide the properties, name, and some layout hints as arguments. All Custom Views should be registered during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#customizing-airship) to ensure the view is available before a Scene is rendered. #### Kotlin ```kotlin // Return an Android View AirshipCustomViewManager.register("my-custom-view") { context, args -> TextView(context).apply { text = args.properties.optionalField("text") ?: "Fallback" } } // For compose, use a ComposeView AirshipCustomViewManager.register("my-custom-view") { context, args -> ComposeView(context).apply { setContent { MaterialTheme { Text(args.properties.optionalField("text") ?: "Fallback") } } } } ``` #### Java ```java // Return an Android View AirshipCustomViewManager.register("my-custom-view", (context, args) -> { TextView textView = new TextView(context); String text = args.getProperties().optionalField(String.class, "text"); textView.setText(text != null ? text : "Fallback"); return textView; }); // For compose, use a ComposeView AirshipCustomViewManager.register("my-custom-view", (context, args) -> { ComposeView composeView = new ComposeView(context); String text = args.getProperties().optionalField(String.class, "text"); composeView.setContent(content -> { MaterialTheme.INSTANCE.invoke(content, theme -> { Text.INSTANCE.invoke(text != null ? text : "Fallback"); }); }); return composeView; }); ``` ## Example custom view The following example shows a Custom View that renders an embedded map when called to render a Custom View named `map`. In our example, we have `properties` that defines a single `place` field, which is the address of the location that the map should render. #### Kotlin First, define the view handler: ```kotlin /** * Custom View that displays a Google Map with a marker at a specified `place`. * * Roughly based on the [Compose Google Map implementation](https://github.com/googlemaps/android-maps-compose/blob/main/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt) */ class MapCustomViewHandler: AirshipCustomViewHandler { override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View { val mapView = MapView(context) val lifecycleObserver = MapLifecycleEventObserver(mapView) val onAttachStateListener = object : View.OnAttachStateChangeListener { private var lifecycle: Lifecycle? = null override fun onViewAttachedToWindow(v: View) { lifecycle = mapView.findViewTreeLifecycleOwner()!!.lifecycle.also { it.addObserver(lifecycleObserver) } } override fun onViewDetachedFromWindow(v: View) { lifecycle?.removeObserver(lifecycleObserver) lifecycle = null lifecycleObserver.moveToBaseState() } } mapView.addOnAttachStateChangeListener(onAttachStateListener) val place: String = args.properties.requireField("place") mapView.getMapAsync { map -> onMapReady(context, map, place) } return mapView } private fun onMapReady(context: Context, map: GoogleMap, place: String) { val geocoder = Geocoder(context) val location = geocoder.getFromLocationName(place, 1).orEmpty() if (location.isNotEmpty()) { val latitude = location[0].latitude val longitude = location[0].longitude val latLng = LatLng(latitude, longitude) // Set up the map with the location map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 10f)) // Optionally, add a marker at the location map.addMarker(MarkerOptions().position(latLng).title(place)) } else { // Show error toast Toast.makeText(context, "Location '$place' not found", Toast.LENGTH_SHORT).show() } } } /** * A [LifecycleEventObserver] that manages the lifecycle of a [MapView]. * * This is used to ensure that the [MapView] is properly managed by the Android lifecycle. */ private class MapLifecycleEventObserver(private val mapView: MapView) : LifecycleEventObserver { private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { when (event) { // [mapView.onDestroy] is only invoked from AndroidView->onRelease. Lifecycle.Event.ON_DESTROY -> moveToBaseState() else -> moveToLifecycleState(event.targetState) } } /** * Move down to [Lifecycle.State.CREATED] but only if [currentLifecycleState] is actually above that. * It's theoretically possible that [currentLifecycleState] is still in [Lifecycle.State.INITIALIZED] state. * */ fun moveToBaseState() { if (currentLifecycleState > Lifecycle.State.CREATED) { moveToLifecycleState(Lifecycle.State.CREATED) } } fun moveToDestroyedState() { if (currentLifecycleState > Lifecycle.State.INITIALIZED) { moveToLifecycleState(Lifecycle.State.DESTROYED) } } private fun moveToLifecycleState(targetState: Lifecycle.State) { while (currentLifecycleState != targetState) { when { currentLifecycleState < targetState -> moveUp() currentLifecycleState > targetState -> moveDown() } } } private fun moveDown() { val event = Lifecycle.Event.downFrom(currentLifecycleState) ?: error("no event down from $currentLifecycleState") invokeEvent(event) } private fun moveUp() { val event = Lifecycle.Event.upFrom(currentLifecycleState) ?: error("no event up from $currentLifecycleState") invokeEvent(event) } private fun invokeEvent(event: Lifecycle.Event) { when (event) { Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) Lifecycle.Event.ON_START -> mapView.onStart() Lifecycle.Event.ON_RESUME -> mapView.onResume() Lifecycle.Event.ON_PAUSE -> mapView.onPause() Lifecycle.Event.ON_STOP -> mapView.onStop() Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() else -> error("Unsupported lifecycle event: $event") } currentLifecycleState = event.targetState } } ``` Then register the view: ```kotlin AirshipCustomViewManager.register("map", MapCustomViewHandler()) ``` #### Java First, define the view handler: ```java /** * Custom View that displays a Google Map with a marker at a specified place. */ public class MapCustomViewHandler implements AirshipCustomViewHandler { @Override public View onCreateView(@NonNull Context context, @NonNull AirshipCustomViewArguments args) { MapView mapView = new MapView(context); String place = args.getProperties().requireField(String.class, "place"); mapView.getMapAsync(map -> onMapReady(context, map, place)); return mapView; } private void onMapReady(Context context, GoogleMap map, String place) { Geocoder geocoder = new Geocoder(context); try { List
addresses = geocoder.getFromLocationName(place, 1); if (!addresses.isEmpty()) { Address address = addresses.get(0); LatLng latLng = new LatLng(address.getLatitude(), address.getLongitude()); map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 10f)); map.addMarker(new MarkerOptions().position(latLng).title(place)); } else { Toast.makeText(context, "Location '" + place + "' not found", Toast.LENGTH_SHORT).show(); } } catch (IOException e) { Toast.makeText(context, "Error geocoding: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } } ``` Then register the view: ```java AirshipCustomViewManager.register("map", new MapCustomViewHandler()); ``` ## Scene Control Custom views control their parent scene through the `SceneController`, which is provided in `AirshipCustomViewArguments`. ### Accessing SceneController Access the scene controller via `args.sceneController` in your view handler's `onCreateView` method. #### Kotlin ```kotlin class CustomViewHandler : AirshipCustomViewHandler { override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View { // Access via args.sceneController args.sceneController.dismiss() return yourView } } ``` #### Java ```java public class CustomViewHandler implements AirshipCustomViewHandler { @Override public View onCreateView(@NonNull Context context, @NonNull AirshipCustomViewArguments args) { // Access via args.getSceneController() args.getSceneController().dismiss(); return yourView; } } ``` ### Dismissing scenes Call `dismiss()` to close the scene, or set `cancelFutureDisplays` to prevent it from displaying again. #### Kotlin ```kotlin // Simple dismiss args.sceneController.dismiss() // Dismiss and cancel future displays args.sceneController.dismiss(cancelFutureDisplays = true) ``` #### Java ```java // Simple dismiss args.getSceneController().dismiss(); // Dismiss and cancel future displays args.getSceneController().dismiss(true); ``` ### Pager navigation Navigate between pages using `navigate()`, which returns `true` if navigation succeeded. #### Kotlin ```kotlin // Navigate forward or backward args.sceneController.pager.navigate(NavigationRequest.NEXT) args.sceneController.pager.navigate(NavigationRequest.BACK) // Check if navigation succeeded val success = args.sceneController.pager.navigate(NavigationRequest.NEXT) ``` #### Java ```java // Navigate forward or backward args.getSceneController().getPager().navigate(NavigationRequest.NEXT); args.getSceneController().getPager().navigate(NavigationRequest.BACK); // Check if navigation succeeded boolean success = args.getSceneController().getPager().navigate(NavigationRequest.NEXT); ``` Observe the pager's `StateFlow` to check current navigation state and enable/disable navigation UI. #### Kotlin ```kotlin // Compose val state = args.sceneController.pager.state.collectAsState() if (state.value.canGoNext) { // Can navigate forward } // View-based lifecycleScope.launch { args.sceneController.pager.state.collect { state -> // Update UI based on state.canGoBack and state.canGoNext } } ``` #### Java ```java LifecycleOwner lifecycleOwner = // get from context CoroutineScope scope = LifecycleScopeKt.getLifecycleScope(lifecycleOwner); FlowKt.launchIn( FlowKt.onEach(args.getSceneController().getPager().getState(), state -> { // Update UI based on state.getCanGoBack() and state.getCanGoNext() return Unit.INSTANCE; }), scope ); ``` ### Sizing management Use `args.sizeInfo` to determine appropriate sizing for your custom view. #### Kotlin ```kotlin // Compose val modifier = when { args.sizeInfo.isAutoWidth && args.sizeInfo.isAutoHeight -> Modifier.wrapContentSize() args.sizeInfo.isAutoWidth -> Modifier.wrapContentWidth().fillMaxHeight() args.sizeInfo.isAutoHeight -> Modifier.fillMaxWidth().wrapContentHeight() else -> Modifier.fillMaxSize() } // View-based val width = if (args.sizeInfo.isAutoWidth) { ViewGroup.LayoutParams.WRAP_CONTENT } else { ViewGroup.LayoutParams.MATCH_PARENT } val height = if (args.sizeInfo.isAutoHeight) { ViewGroup.LayoutParams.WRAP_CONTENT } else { ViewGroup.LayoutParams.MATCH_PARENT } ``` #### Java ```java // View-based int width = args.getSizeInfo().isAutoWidth() ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; int height = args.getSizeInfo().isAutoHeight() ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; ``` ![Map Custom View in a Scene on Android](https://www.airship.com/docs/images/custom-views-map-scene-android_hu_9a022a240a253d3a.webp) *Map Custom View in a Scene on Android* ## Embedding Airship Views Airship views like Preference Center can be embedded as custom views. #### Kotlin ```kotlin import com.urbanairship.preferencecenter.compose.ui.PreferenceCenterContent import com.urbanairship.preferencecenter.compose.ui.PreferenceCenterScreen import com.urbanairship.preferencecenter.ui.PreferenceCenterFragment // Compose - Content only (no navigation bar) AirshipCustomViewManager.register("preference_center_content") { context, args -> val id = args.properties.opt("id").optString() ComposeView(context).apply { setContent { PreferenceCenterContent( identifier = id, modifier = Modifier.fillMaxSize() ) } } } // Compose - Full preference center with navigation bar AirshipCustomViewManager.register("preference_center") { context, args -> val id = args.properties.opt("id").optString() ComposeView(context).apply { setContent { PreferenceCenterScreen( identifier = id, modifier = Modifier.fillMaxSize(), onNavigateUp = { args.sceneController.dismiss() } ) } } } // View - Content only (no navigation bar) AirshipCustomViewManager.register("preference_center_content") { context, args -> val id = args.properties.opt("id").optString() val activity = context as? FragmentActivity ?: throw IllegalStateException("Context must be FragmentActivity") FrameLayout(context).apply { id = View.generateViewId() activity.supportFragmentManager.beginTransaction() .add(id, PreferenceCenterFragment.create(id)) .commitNow() } } // View - Full preference center with navigation bar AirshipCustomViewManager.register("preference_center") { context, args -> val id = args.properties.opt("id").optString() val activity = context as? FragmentActivity ?: throw IllegalStateException("Context must be FragmentActivity") LinearLayout(context).apply { orientation = LinearLayout.VERTICAL val toolbar = MaterialToolbar(context).apply { setNavigationIcon(R.drawable.abc_ic_ab_back_material) setNavigationOnClickListener { args.sceneController.dismiss() } } addView(toolbar, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) val container = FragmentContainerView(context).apply { id = View.generateViewId() } addView(container, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) activity.supportFragmentManager.beginTransaction() .add(container.id, PreferenceCenterFragment.create(id)) .commitNow() } } ``` #### Java ```java import com.urbanairship.preferencecenter.compose.ui.PreferenceCenterContent; import com.urbanairship.preferencecenter.compose.ui.PreferenceCenterScreen; import com.urbanairship.preferencecenter.ui.PreferenceCenterFragment; // Compose - Content only (no navigation bar) AirshipCustomViewManager.register("preference_center_content", (context, args) -> { String id = args.getProperties().opt("id").optString(); ComposeView composeView = new ComposeView(context); composeView.setContent(() -> { PreferenceCenterContent(id, Modifier.fillMaxSize(), null); }); return composeView; }); // Compose - Full preference center with navigation bar AirshipCustomViewManager.register("preference_center", (context, args) -> { String id = args.getProperties().opt("id").optString(); ComposeView composeView = new ComposeView(context); composeView.setContent(() -> { PreferenceCenterScreen( id, Modifier.fillMaxSize(), () -> { args.getSceneController().dismiss(); return Unit.INSTANCE; } ); }); return composeView; }); // View - Content only (no navigation bar) AirshipCustomViewManager.register("preference_center_content", (context, args) -> { String id = args.getProperties().opt("id").optString(); FragmentActivity activity = (FragmentActivity) context; FrameLayout layout = new FrameLayout(context); int containerId = View.generateViewId(); layout.setId(containerId); activity.getSupportFragmentManager() .beginTransaction() .add(containerId, PreferenceCenterFragment.create(id)) .commitNow(); return layout; }); // View - Full preference center with navigation bar AirshipCustomViewManager.register("preference_center", (context, args) -> { String id = args.getProperties().opt("id").optString(); FragmentActivity activity = (FragmentActivity) context; LinearLayout layout = new LinearLayout(context); layout.setOrientation(LinearLayout.VERTICAL); MaterialToolbar toolbar = new MaterialToolbar(context); toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material); toolbar.setNavigationOnClickListener(v -> args.getSceneController().dismiss()); layout.addView(toolbar, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); FragmentContainerView container = new FragmentContainerView(context); int containerId = View.generateViewId(); container.setId(containerId); layout.addView(container, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); activity.getSupportFragmentManager() .beginTransaction() .add(containerId, PreferenceCenterFragment.create(id)) .commitNow(); return layout; }); ``` ![Preference Center Custom View in a Scene on Android](https://www.airship.com/docs/images/custom-views-preference-center-scene-android_hu_a81aa6610c793dfd.webp) *Preference Center Custom View in a Scene on Android* # In-App Automation > In-app messages are fully customizable. Minor modifications can be accomplished by overriding the styles, but more advanced customizations can employ an adapter for the given message type. In-App Automation (IAA) powers banner, modal, and fullscreen in-app messages. ## Customization Various options are available for customizing the container view for In-App Automation content via the native SDKs. Scenes are fully customizable in the dashboard and cannot be customized via the SDK. ## Excluding Activities Activities can be excluded from auto-displaying an in-app message. This is useful for preventing in-app message displays on screens where it would be detrimental to the user experience, such as splash screens, settings screens, or landing pages. **Exclude activity from displaying in-app messages** ```xml ``` ## Listening for Events A listener can be added to the in-app messaging manager to listen for when a message is displayed and finished displaying. This is useful for adding analytics events outside of Airship as well as for further processing of the in-app message. #### Kotlin ```kotlin InAppAutomation.shared().inAppMessaging.displayDelegate = object : InAppMessageDisplayDelegate { override fun isMessageReadyToDisplay(message: InAppMessage, scheduleId: String): Boolean { // Called to check if the message is ready to be displayed return true } override fun messageWillDisplay(message: InAppMessage, scheduleId: String) { // Message displayed } override fun messageFinishedDisplaying(message: InAppMessage, scheduleId: String) { // Message finished } } ``` #### Java ```java InAppAutomation.shared().getInAppMessaging().setDisplayDelegate(new InAppMessageDisplayDelegate() { @Override public boolean isMessageReadyToDisplay(@NonNull InAppMessage message, @NonNull String scheduleId) { // Called to check if the message is ready to be displayed return true; } @Override public void messageWillDisplay(@NonNull InAppMessage message, @NonNull String scheduleId) { // Message displayed } @Override public void messageFinishedDisplaying(@NonNull InAppMessage message, @NonNull String scheduleId) { // Message finished } }); ``` ## Fonts Fonts that are added in XML are available for use with in-app messaging, including downloadable fonts. To add fonts, please read the [Fonts In XML guide](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml). ![Android Custom Font](https://www.airship.com/docs/images/android/android-custom-font_hu_3fb1c1887a6d73ac.webp) *Android Custom Font* After adding fonts to your app, create a Font Stack in the Airship dashboard by following the steps in [Setting brand guidelines](https://www.airship.com/docs/guides/messaging/features/brand-guidelines/). Then you can select the stack when [setting in-app message defaults](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/defaults/) and creating in-app messages. ## Styles The Android resource merging feature can be used to override any of the message styles that the SDK provides. Copy any of the styles that need to be overridden into the application's resource directory, then change any of the styles. Styles: - [Banner](https://github.com/urbanairship/android-library/blob/main/urbanairship-automation/src/main/res/values/style_iam_banner.xml) - [Fullscreen](https://github.com/urbanairship/android-library/blob/main/urbanairship-automation/src/main/res/values/style_iam_fullscreen.xml) - [Modal](https://github.com/urbanairship/android-library/blob/main/urbanairship-automation/src/main/res/values/style_iam_modal.xml) - [HTML](https://github.com/urbanairship/android-library/blob/main/urbanairship-automation/src/main/res/values/style_iam_html.xml) ## Custom Adapters Providing an adapter allows full customization for any message type. The adapter will be created by the in-app messaging manager when a message's schedule is triggered. Once created, the adapter will be called to first prepare the in-app message, giving the adapter time to download any resources such as images. After the adapter prepares the message, the adapter will be called to display the message. Be sure to update the adapter factory for the message type you are trying to override. In this example, the adapter is overriding modal messages, therefore the custom display adapter type is `CustomDisplayAdapterType.MODAL`. After the message is displayed, the provided display handler must be notified that the message is finished displaying by returning a `CustomDisplayResolution` via the suspending method or callback. This will allow other in-app messages to be displayed. #### Kotlin ```kotlin class MyCustomDisplayAdapter( private val context: Context, private val message: InAppMessage, private val assets: AirshipCachedAssets, private val scope: CoroutineScope ) : CustomDisplayAdapter.SuspendingAdapter { // Implement the isReady property, used to signal when the adapter is ready to display the message. // If this adapter does not need to wait for anything before displaying the message, you can return // an initial value of true to indicate that it is always ready. Otherwise, set the initial value // to false, and emit true once the adapter is ready to display the message. override val isReady: StateFlow = MutableStateFlow(true) override fun display(context: Context): CustomDisplayResolution { // Display the message... return CustomDisplayResolution.UserDismissed } companion object { fun register(scope: CoroutineScope) { InAppAutomation.shared().inAppMessaging.setAdapterFactoryBlock( type = CustomDisplayAdapterType.MODAL, factoryBlock = { context, message, assets -> MyCustomDisplayAdapter(context, message, assets, scope) } ) } } } ``` #### Java ```java class MyCustomDisplayAdapter implements CustomDisplayAdapter.CallbackAdapter { private final InAppMessage message; public MyCustomDisplayAdapter( Context context, InAppMessage message, AirshipCachedAssets assets ) { this.message = message; } @Override public void display(@NonNull Context context, DisplayFinishedCallback callback) { // Display the message... callback.finished(CustomDisplayResolution.UserDismissed.INSTANCE); } static void register() { InAppAutomation.shared().getInAppMessaging().setAdapterFactoryBlock( CustomDisplayAdapterType.MODAL, (context, message, assets) -> new MyCustomDisplayAdapter(context, message, assets) ); } } ``` #### Kotlin ```kotlin MyCustomDisplayAdapter.register() ``` #### Java ```java MyCustomDisplayAdapter.register(); ``` ## Standard In-App Messages Standard [in-app messages](https://www.airship.com/docs/guides/messaging/messages/content/app/in-app-messages/) delivered through push messages are managed by the legacy in-app message manager. The manager converts the standard in-app message into a new in-app message schedule. The conversion can be customized by setting a builder extender to extend either the schedule builder or the message builder. #### Kotlin ```kotlin InAppAutomation.shared().legacyInAppMessaging .scheduleExtender = { schedule -> // Return an updated schedule schedule.newBuilder() .setPriority(10) .setLimit(3) .build() } ``` #### Java ```java InAppAutomation.shared().getLegacyInAppMessaging() .setScheduleExtender((schedule) -> // Return an updated schedule schedule.newBuilder() .setPriority(10) .setLimit(3) .build() ); ``` #### Kotlin ```kotlin InAppAutomation.shared().legacyInAppMessaging .messageExtender = { message -> val extras = JsonMap.newBuilder() .putAll(message.getExtras()) .put("custom_key", "custom_value") .build() // Return an updated message message.newBuilder() .setExtras(extras) .build() } ``` #### Java ```java InAppAutomation.shared().getLegacyInAppMessaging() .setMessageExtender(message -> { JsonMap extras = JsonMap.newBuilder() .putAll(message.getExtras()) .put("custom_key", "custom_value") .build(); // Return an updated message return message.newBuilder() .setExtras(extras) .build(); }); ``` ## Customizing HTML In-App Messages > **Note:** In order for the Airship JavaScript interface to be loaded into the webview, the URL must be specified in the [UrlAllowList](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#configuring-airship). HTML in-app messages provide a way to display custom content inside a native web view. These types of in-app messages display with a dismiss button built in, but can also be customized to provide their own buttons capable of dismissing the view. Dismissing a view requires calling the dismiss function on the UAirship JavaScript interface with a button resolution object passed in as a parameter. The button resolution object is a JSON object containing information about the interaction type and the button performing the dismissal. It should match the following format: ```javascript { "type" : "button_click", "button_info" : { "id" : "button identifier", "label" : {"text" : "foo"} } } ``` The button resolution requires each of the key fields shown above. These include: - `type` — The type key with the value of resolution type `button_click` - `button_info` — The button info object containing required id and label fields - `id` — The button identifier - `label` — Label object containing the required text key - `text` — The text key with a string value representing the label text Providing a basic dismiss button in HTML: ```html ``` ### Message Center Implement Message Center to provide an inbox for rich HTML-based messages, including display, theming, embedding, and advanced customization. # Message Center > Message Center provides an inbox for rich HTML-based messages that users can view at their convenience, with support for custom theming and display handling. ![Message Center inbox on Android](https://www.airship.com/docs/images/message-center-android.webp) *Message Center inbox on Android* Message Center provides an inbox for rich, HTML-based messages that users can view at their convenience. By default, when your app receives a push notification with a Message Center action, the Message Center automatically displays in a modal activity. The Message Center can also be displayed manually by calling a simple method, making it easy to add a Message Center button to your app's navigation. Message Center inboxes are associated with channel IDs, which persist across app launches, allowing users to access their message history. Airship provides two distinct modules for displaying Message Center on Android, depending on your app's UI framework: - **`urbanairship-message-center-compose`**: Provides Compose-based Message Center UI components, for apps built with Jetpack Compose. - **`urbanairship-message-center`**: Provides XML-based Message Center UI components, for apps using traditional Android Views (XML layouts). You should select only one module based on your UI framework—do not include both modules in the same app. For more information about Message Center messages, see the [Message Center feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Installation Include the correct module for your chosen UI framework: #### Jetpack Compose ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-message-center-compose:$airshipVersion") } ``` #### XML Views ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-message-center:$airshipVersion") } ``` ## Display the Message Center Display the Message Center with a single method call: #### Kotlin ```kotlin Airship.messageCenter.showMessageCenter() ``` #### Java ```java MessageCenter.shared().showMessageCenter(); ``` This displays the Message Center as a modal activity, allowing users to view and manage their messages. When the user closes the Message Center, any changes (such as marking messages as read) are automatically synced with Airship. > **Note:** To embed the Message Center directly in your app's navigation instead of displaying it as an overlay, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/android/message-center/embedding/). You can also [intercept display requests](https://www.airship.com/docs/developer/sdk-integration/android/message-center/embedding/#handling-display-requests) to handle navigation to your embedded Message Center. ## Applying a Custom Theme You can customize the appearance of the Message Center to match your app's style. Android supports theme customization through both Jetpack Compose and XML Views. #### Jetpack Compose To apply a custom theme to the ready-to-use Message Center UI, create a theme and set it on the `MessageCenter` instance early in your app's lifecycle. The overridden `onAirshipReady()` method in your Autopilot class is a good place to do this. **Customizing the theme with Jetpack Compose** ```kotlin // Configure Message Center Theme val messageCenterTheme = MessageCenterTheme( lightColors = MessageCenterColors.lightDefaults( background = Color(0xDEDEDE), surface = Color(0xFFFFFF), accent = Color(0x6200EE), ), darkColors = MessageCenterColors.darkDefaults( background = Color(0x121212), surface = Color(0x1E1E1E), accent = Color(0xBB86FC), ), typography = MessageCenterTypography.defaults( fontFamily = FontFamily(context.resources.getFont(R.font.roboto_regular)) ) ) // Apply theme to default Message Center UI Airship.messageCenter.theme = messageCenterTheme ``` #### XML Views The ready-to-use Message Center UI uses the `UrbanAirship.MessageCenter` style. You can use xml resource merging to override the default styles, by defining the style in your app. ### Theme Attributes The Message Center supports the following theme attributes: **messageCenterToolbarTitle** : String to use for the Message Center toolbar title **messageCenterIconsEnabled** : Flag to enable message thumbnails in the message list **messageCenterPlaceholderIcon** : The default placeholder image for message thumbnails **messageCenterItemDividersEnabled** : Flag to enable dividers between messages in the list **messageCenterItemDividerInsetStart** : The start inset for message list dividers **messageCenterItemDividerInsetEnd** : The end inset for message list dividers **dividerColor** (set via Material Theme) : The message list divider color, if dividers are enabled **Extending from a Material3 app theme** ```xml ``` > **Note:** If your app doesn't use a `Material3` theme or you need the ability to further customize Message Center styles, the Android resource merging feature can be used to override the default styles that the SDK provides. Copy the [style sheet](https://github.com/urbanairship/android-library/blob/master/urbanairship-message-center/src/main/res/values/style_message_center.xml) > into the application's resource directory, then change any of the styles. ## Working with Messages The Message Center provides methods to fetch, mark as read, and delete messages programmatically. ### Fetch Messages Retrieve messages from the inbox: #### Kotlin ```kotlin // Suspending call scope.launch { val messages = Airship.messageCenter.inbox.getMessages() } // Flow scope.launch { // Collect the messages flow, which emits a new list whenever the inbox is updated Airship.messageCenter.inbox.getMessagesFlow().collect { messages -> // Handle messages } } ``` #### Java ```java PendingResult> messagesResult = MessageCenter.shared().getInbox().getMessagesPendingResult(); messagesResult.addResultCallback(messages -> { // Handle messages }); ``` ### Listen for Message Updates Subscribe to message updates using a listener or Flow: #### Kotlin ```kotlin // Option 1: Messages Flow scope.launch { Airship.messageCenter.inbox.getMessagesFlow().collect { messages -> // Update your UI with the new messages } } // Option 2: InboxListener Airship.messageCenter.inbox.addListener(object: InboxListener { override fun onInboxUpdated() { // Update your UI } }) ``` #### Java ```java MessageCenter.shared().getInbox().addListener(() -> { // Update your UI }); ``` ### Listen for Unread Count Changes Subscribe to unread count updates: #### Kotlin ```kotlin scope.launch { Airship.messageCenter.inbox.getUnreadCountFlow().collect { unreadCount -> // Update badge or UI } } ``` #### Java ```java MessageCenter.shared().getInbox().getUnreadCountPendingResult() .addResultCallback(unreadCount -> { // Update badge or UI }); ``` ### Refresh Messages Manually refresh the message list from the server: #### Kotlin ```kotlin Airship.messageCenter.inbox.fetchMessages { success -> // Handle result } ``` #### Java ```java MessageCenter.shared().getInbox().fetchMessages(new Inbox.FetchMessagesCallback() { @Override public void onFinished(boolean success) { // Handle the result } }); ``` ### Mark Messages as Read Mark one or more messages as read: #### Kotlin ```kotlin Airship.messageCenter.inbox.markMessagesRead(messageId) ``` #### Java ```java MessageCenter.shared().getInbox().markMessagesRead("messageId"); ``` ### Delete Messages Delete one or more messages: #### Kotlin ```kotlin Airship.messageCenter.inbox.deleteMessages("messageId") ``` #### Java ```java MessageCenter.shared().getInbox().deleteMessages("messageId"); ``` ## Filter Messages by Named User By default, Message Center displays all messages sent to the device's channel. If multiple users log into your app on the same device, they'll all see the same messages. To filter messages by named user, set up filtering in your custom Message Center implementation. See [Message Center Filtering](https://www.airship.com/docs/developer/sdk-integration/android/message-center/embedding/#message-center-filtering) in the Embedding guide. When creating Message Center messages, include a custom key with `named_user_id` as the key and the user's actual ID as the value: - **For the API**: Use the `extra` object in the [Message Center object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#messageobject). - **In the dashboard**: See [Add custom keys](https://www.airship.com/docs/guides/messaging/messages/content/app/message-center/#add-custom-keys) in the Message Center content guide. ### Filtering Behavior With named user filtering enabled: - If you target `User A` in a message while they are logged in, the message appears in their inbox. - If you target `User B` in a message while they are logged in, the message appears in their inbox. - If you target `User A` or `User B` while the other is logged in, the message does not appear. - If you target `User A` or `User B` while neither is logged in, the message does not appear. # Embed the Message Center > Embed the Message Center directly in your app's navigation, create custom implementations with full control over design and functionality, and filter messages by named user. This guide covers how to embed the Message Center directly in your app's navigation instead of displaying it as an overlay, create custom implementations, and filter messages. ## Handling Display Requests To use a custom Message Center implementation or navigate to your embedded Message Center instead of the default overlay activity, set a listener to handle display requests: #### Kotlin ```kotlin Airship.messageCenter.setOnShowMessageCenterListener { messageId: String? -> // Navigate to your custom Message Center UI // messageId is optional - null means show the full message list // Return true to prevent the default SDK display true } ``` #### Java ```java MessageCenter.shared().setOnShowMessageCenterListener(messageId -> { // Navigate to your custom Message Center UI // messageId is optional - null means show the full message list // Return true to prevent the default SDK display return true; }); ``` ## Embedding with Jetpack Compose When embedding Message Center composables, you can choose between an all-in-one screen with a customizable top bar and a content-only Message Center view, without a top bar. Both composables must be wrapped in a `MessageCenterTheme`, which allows the theme to be customized. **MessageCenterScreen** ```kotlin MessageCenterTheme { MessageCenterScreen() } ``` **MessageCenterContent** ```kotlin MessageCenterTheme { MessageCenterContent() } ``` **Customizing the theme** ```kotlin val lightColors = MessageCenterColors.lightDefaults( background = Color(0xDEDEDE), surface = Color(0xFFFFFF), accent = Color(0x6200EE), ) val darkColors = MessageCenterColors.darkDefaults( background = Color(0x121212), surface = Color(0x1E1E1E), accent = Color(0xBB86FC), ) val typography = MessageCenterTypography.defaults( fontFamily = FontFamily(context.resources.getFont(R.font.roboto_regular)) ) MessageCenterTheme( colors = if (isSystemInDarkTheme()) darkColors else lightColors, typography = typography ) { // MessageCenterScreen OR MessageCenterContent } ``` ## Embedding with XML Views The Message Center UI can be embedded in any `FragmentActivity` or `Fragment` using `MessageCenterFragment`. When embedding the MessageCenterFragment, either use a `FragmentContainerView` or create the fragment directly. ### Using FragmentContainerView Add the `MessageCenterFragment` directly in your layout XML: ```xml ``` ### Creating Fragment Programmatically Alternatively, create and add the fragment programmatically: #### Kotlin ```kotlin val fragment = MessageCenterFragment.newInstance() ``` #### Java ```java MessageCenterFragment fragment = MessageCenterFragment.newInstance(); ``` You will need to [set up display request handling](#handling-display-requests) to navigate to your embedded fragment instead of letting Airship launch the `MessageCenterActivity`. ### Integrating with Navigation For more control over the UI, `MessageCenterListFragment` and `MessageCenterMessageFragment` can be used to embed the list and message views separately, each maintaining its own `Toolbar`. This example assumes that Jetpack Navigation is being used to navigate between the list and message views, but any navigation method can be used. ### Custom Message List Fragment ```kotlin class CustomMessageListFragment() : MessageCenterListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val toolbar = view.findViewById(R.id.toolbar) toolbar.inflateMenu(messageCenterR.menu.ua_message_center_list_pane_menu) // Set up the toolbar, if desired. setupWithNavController(toolbar, findNavController()) onMessageClickListener = OnMessageClickListener { // Handle message clicks by navigating to the message fragment // (or replace with custom navigation logic). findNavController().navigate( R.id.action_messageCenterFragment_to_messageFragment, bundleOf( MessageCenterMessageFragment.ARG_MESSAGE_ID to it.id, MessageCenterMessageFragment.ARG_MESSAGE_TITLE to it.title ) ) } } } ``` ### Custom Message Fragment ```kotlin class CustomMessageFragment : MessageCenterMessageFragment(R.layout.fragment_inbox_message) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) toolbar?.run { // Inflate the default menu inflateMenu(messageCenterR.menu.ua_message_center_message_pane_menu) // Set up the toolbar, if desired. setupWithNavController(toolbar, findNavController(view)) } // Handle message deletion from the message view onMessageDeletedListener = OnMessageDeletedListener { // Handle message deletion by navigating back to the message list fragment // (or replace with custom navigation logic). findNavController().popBackStack() // Optionally show a toast confirmation message context?.run { val msg = getQuantityString(messageCenterR.plurals.ua_mc_description_deleted, 1, 1) Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() } } } } ``` These custom fragments can then be embedded into your app UI using `FragmentContainerView` or by using `FragmentManager` programmatically. You will need to [set up display request handling](#handling-display-requests) to navigate to your custom message list fragment instead of letting Airship launch the `MessageCenterActivity`. ## Layout Support The Message Center provides automatic support for both single-pane and two-pane layouts, when on large display sizes or foldable devices. In two-pane mode, the selected message is highlighted in the list and displayed in the detail pane. Right-to-left (RTL) layout is also supported when `android:supportsRtl="true"` is set in your application manifest. ## Message Center Filtering Sometimes it can be useful to filter the contents of the Message Center according to some predetermined pattern. To facilitate this, use the shared `MessageCenter` instance to set a predicate. Once set, only messages that match the predicate will be displayed. ### Filter by Named User Filter messages to show only those for the current named user: #### Kotlin ```kotlin MessageCenter.shared().predicate = Predicate { message -> val namedUserID = Airship.shared().contact.namedUserID if (namedUserID == null) { return@Predicate false } // Check if message has matching named_user_id in extras val extras = message.extras val messageNamedUserID = extras?.get("named_user_id") as? String messageNamedUserID == namedUserID } ``` #### Java ```java MessageCenter.shared().setPredicate(message -> { String namedUserID = Airship.shared().getContact().getNamedUserID(); if (namedUserID == null) { return false; } // Check if message has matching named_user_id in extras Map extras = message.getExtras(); String messageNamedUserID = extras != null ? extras.get("named_user_id") : null; return messageNamedUserID != null && messageNamedUserID.equals(namedUserID); }); ``` ### Custom Filtering Create custom predicates for any filtering logic: #### Kotlin ```kotlin MessageCenter.shared().predicate = Predicate { message -> // Example: Only show messages with "cool" in the title message.title.contains("cool") } ``` #### Java ```java MessageCenter.shared().setPredicate(message -> // Example: Only show messages with "cool" in the title message.getTitle().contains("cool") ); ``` ## Custom Message Center Implementation For complete control over Message Center placement and navigation, create a custom implementation using the Message Center components. ### Key Components **MessageCenter** : The main entry point for fetching messages and handling callbacks. Access via `MessageCenter.shared()`. **Inbox** : Provides an interface for retrieving messages asynchronously and accessing the local message array. Access via `MessageCenter.shared().inbox`. > **Note:** The message list uses a local database. Message objects are refreshed with the list. Don't hold onto individual message instances indefinitely. **Message** : Model object representing an individual message. Instances don't contain the message body—they point to authenticated URLs that should be displayed in a webview. **Display Callbacks** : Set `MessageCenter.shared().setOnShowMessageCenterListener()` to handle when messages should be displayed. ### Preference Center Implement Preference Centers to allow users to manage their subscription preferences, including display, theming, and embedding options. # Preference Center > Display Preference Centers using the Airship UI, which automatically handles user preferences and syncs with the Airship backend. ![Preference Center on Android](https://www.airship.com/docs/images/preference-center-android_hu_84f8445eff5c8fa8.webp) *Preference Center on Android* > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. The Preference Center feature allows users to manage their subscription list preferences as configured in the Airship Dashboard. Airship provides two distinct modules for displaying Preference Centers on Android, depending on your app's UI framework: - **`urbanairship-preference-center-compose`**: Provides Compose-based Preference Center UI components, for apps built with Jetpack Compose. - **`urbanairship-preference-center`**: Provides XML-based Preference Center UI components, for apps using traditional Android Views (XML layouts). You should select only one module based on your UI framework—do not include both modules in the same app. For more information about configuring Preference Centers, see the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Installation Include the correct module for your chosen UI framework: #### Jetpack Compose ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-preference-center-compose:$airshipVersion") } ``` #### XML Views ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-preference-center:$airshipVersion") } ``` ## Displaying a Preference Center Display a Preference Center with a single method call. The Preference Center will be launched in its own Activity over your app, allowing users to manage their subscription preferences, and automatically syncing changes with Airship. #### Kotlin ```kotlin Airship.preferenceCenter.open("my-first-pref-center") ``` #### Java ```java PreferenceCenter.shared().open("my-first-pref-center"); ``` > **Note:** To embed the Preference Center directly in your app's navigation instead of displaying it as an overlay, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/android/preference-center/embedding/). You can also [intercept display requests](#override-default-display-behavior) to handle navigation to your embedded Preference Center. ## Applying a Custom Theme You can customize the appearance of the Preference Center to match your app's style. Android supports theme customization through both Jetpack Compose and XML Views. #### Jetpack Compose To apply a custom theme to the ready-to-use Preference Center UI, create a theme and set it on the `PreferenceCenter` instance early in your app's lifecycle. The overridden `onAirshipReady()` method in your Autopilot class is a good place to do this. **Customizing the theme with Jetpack Compose** ```kotlin // Configure Preference Center Theme val preferenceCenterTheme = PreferenceCenterTheme( lightColors = PreferenceCenterColors.lightDefaults( background = Color(0xDEDEDE), surface = Color(0xFFFFFF), accent = Color(0x6200EE), ), darkColors = PreferenceCenterColors.darkDefaults( background = Color(0x121212), surface = Color(0x1E1E1E), accent = Color(0xBB86FC), ), typography = PreferenceCenterTypography.defaults( fontFamily = FontFamily(context.resources.getFont(R.font.roboto_regular)) ) ) // Apply theme to default Preference Center UI Airship.preferenceCenter.theme = preferenceCenterTheme ``` #### XML Views The ready-to-use Preference Center UI uses the `UrbanAirship.PreferenceCenter` style. You can use xml resource merging to override the default styles, by defining the style in your app. **Extending from a Material3 app theme** ```xml ``` If your app doesn't use a `Material3` theme or you need the ability to further customize preference center styles, The Android resource merging feature can be used to override the default styles that the SDK provides. Copy the [style sheet](https://github.com/urbanairship/android-library/blob/master/urbanairship-preference-center/src/main/res/values/style_preference_center.xml) into the application's resource directory, then change any of the styles. # Embed the Preference Center > Embed the Preference Center view directly in your app's navigation instead of displaying it as an overlay. This guide covers advanced Preference Center customization options, from styling the default UI to creating fully custom implementations. ## Handling Display Requests To use a custom Preference Center implementation or navigate to your embedded Preference Center instead of the default activity, set a listener to handle showing your custom UI: #### Kotlin Set the `PreferenceCenterOpenListener` during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/). ```kotlin Airship.preferenceCenter.openListener = object : PreferenceCenter.OnOpenListener { override fun onOpenPreferenceCenter(preferenceCenterId: String): Boolean { // Navigate to custom preference center UI // true to prevent default behavior // false for default Airship handling return true } } ``` #### Java Set the `PreferenceCenterOpenListener` during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/). ```java PreferenceCenter.shared().setOpenListener(preferenceCenterId -> { // Navigate to custom preference center UI // true to prevent default behavior // false for default Airship handling return true; }); ``` ## Embedding with Jetpack Compose When embedding Preference Center composables, you can choose between an all-in-one screen with a customizable top bar and a content-only Preference Center view, without a top bar. Both composables must be wrapped in a `PreferenceCenterTheme`, which allows the theme to be customized. **PreferenceCenterScreen** ```kotlin PreferenceCenterTheme { PreferenceCenterScreen(identifier = "my-first-pref-center") } ``` **PreferenceCenterContent** ```kotlin PreferenceCenterTheme { PreferenceCenterContent(identifier = "my-first-pref-center") } ``` **Customizing the theme** ```kotlin val lightColors = PreferenceCenterColors.lightDefaults( background = Color(0xDEDEDE), surface = Color(0xFFFFFF), accent = Color(0x6200EE), ) val darkColors = PreferenceCenterColors.darkDefaults( background = Color(0x121212), surface = Color(0x1E1E1E), accent = Color(0xBB86FC), ) val typography = PreferenceCenterTypography.defaults( fontFamily = FontFamily(context.resources.getFont(R.font.roboto_regular)) ) PreferenceCenterTheme( colors = if (isSystemInDarkTheme()) darkColors else lightColors, typography = typography ) { // PreferenceCenterScreen OR PreferenceCenterContent } ``` ## Embedding with XML Views When embedding the PreferenceCenterFragment, either use a [FragmentContainerView](https://developer.android.com/reference/androidx/fragment/app/FragmentContainerView) or create the fragment directly. You must specify the ID of the Preference center to be displayed when creating the fragment. The static `create` on `PreferenceCenter` will handle passing the given id to the fragment as an argument: #### Kotlin ```kotlin val fragment = PreferenceCenterFragment.create(preferenceCenterId = "my-first-pref-center") ``` #### Java ```java PreferenceCenterFragment fragment = PreferenceCenterFragment.create("my-first-pref-center"); ``` You will need to [override the open behavior](#override-default-display-behavior) to navigate to the embedded fragment instead of letting Airship launch the `PreferenceCenterActivity`. ### Audience Management Integrate audience management features into your app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs, listen for channel creation, and configure the channel capture tool. Each device or app install generates a unique identifier known as the Channel ID. Once a Channel ID is created, it persists in the application until the app is reinstalled or its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. #### Kotlin ```kotlin val channelId = Airship.channel.id ``` #### Java ```java String channelId = Airship.getChannel().getId(); ``` The SDK creates the Channel ID asynchronously, so it may not be available immediately on the first run. The SDK automatically batches and applies changes to Channel data when the Channel is created, so you do not need to wait for the Channel to be available before modifying data. Applications that need to access the Channel ID can use a listener to receive notification when it becomes available. #### Kotlin Using `channelIdFlow` (StateFlow): ```kotlin // channelIdFlow is a StateFlow that emits the channel ID when it's created scope.launch { Airship.channel.channelIdFlow.collect { channelId -> channelId?.let { Log.d("Sample", "Channel created: $it") } } } ``` Using `addChannelListener`: ```kotlin Airship.channel.addChannelListener { channelId -> Log.d("Sample", "Channel created: $channelId") } ``` #### Java Using `addChannelListener`: ```java Airship.getChannel().addChannelListener(new AirshipChannelListener() { @Override public void onChannelCreated(@NonNull String channelId) { // created } }); ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring `AirshipConfigOptions`, see [Android SDK Setup](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/). #### Kotlin ```kotlin val options = airshipConfigOptions { // ... setChannelCaptureEnabled(false) } ``` #### Java ```java AirshipConfigOptions options = AirshipConfigOptions.newBuilder() // ... .setChannelCaptureEnabled(false) .build(); ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during initialization. For more information about Privacy Manager, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/android/data-collection/privacy-manager/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) You can call `identify` multiple times with the same Named User ID. The SDK automatically deduplicates `identify` calls made with the same Named User ID. If you change the ID from a previous value, the SDK automatically dissociates the Contact from the previous Named User ID. #### Kotlin ```kotlin Airship.contact.identify("some named user ID") ``` #### Java ```java Airship.getContact().identify("some named user ID"); ``` If the user logs out of the device, you may want to reset the contact. Resetting clears any anonymous data and dissociates the contact from the Named User ID, if set. Call this method only when the user manually logs out of the app. Otherwise, you cannot target the Channel by its Contact data. #### Kotlin ```kotlin Airship.contact.reset() ``` #### Java ```java Airship.getContact().reset(); ``` You can retrieve the Named User ID only if you set it through the SDK. #### Kotlin ```kotlin Airship.contact.namedUserId ``` #### Java ```java Airship.getContact().getNamedUserId(); ``` ### Email channel association When an email address is registered through the SDK, it will be registered for both transactional and commercial emails by default. To change this behavior, you can override the options to request [[Double Opt-In](https://www.airship.com/docs/reference/glossary/#double_opt_in)](https://www.airship.com/docs/developer/api-integrations/email/getting-started/#double-opt-in) for commercial messages. #### Kotlin ```kotlin val properties = JsonMap.newBuilder().put("place", "paris").build() val options = EmailRegistrationOptions.commercialOptions(commercialDate, transactionalDate, properties) Airship.contact.registerEmail("your@example.com", options) ``` #### Java ```java JsonMap properties = JsonMap.newBuilder().put("place", "paris").build(); EmailRegistrationOptions options = EmailRegistrationOptions.commercialOptions(commercialDate, transactionalDate, properties); Airship.getContact().registerEmail("your@example.com", options); ``` ### SMS channel association When an [MSISDN](https://www.airship.com/docs/reference/glossary/#msisdn) is registered through the SDK, Airship sends a message to that number, prompting them to opt in. For more information, see the SMS platform documentation: [Non-Mobile Double Opt-In](https://www.airship.com/docs/developer/api-integrations/sms/opt-in-out-handling/#non-mobile-double-opt-in). #### Kotlin ```kotlin val options = SmsRegistrationOptions.options("senderId") Airship.contact.registerSms("yourMsisdn", options) ``` #### Java ```java SmsRegistrationOptions options = SmsRegistrationOptions.options("senderId"); Airship.getContact().registerSms("yourMsisdn", options); ``` ### Open Channel association Open Channels support notifications to any medium that can accept a JSON payload, through either the Airship API or web dashboard. For more information about Open Channels, see the [Open Channels documentation](https://www.airship.com/docs/developer/api-integrations/open/getting-started/). #### Kotlin ```kotlin val options = OpenChannelRegistrationOptions.options("platformName") Airship.contact.registerOpenChannel("address", options) ``` #### Java ```java OpenChannelRegistrationOptions options = OpenChannelRegistrationOptions.options("platformName"); Airship.getContact().registerOpenChannel("address", options); ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. #### Kotlin ```kotlin Airship.channel.editTags { addTag("some_tag") removeTag("some_other_tag") } // Accessing channel tags val tags = Airship.channel.tags ``` #### Java ```java Airship.getChannel().editTags() .addTag("some_tag") .removeTag("some_other_tag") .apply(); // Accessing channel tags ArrayList tags = Airship.getChannel().getTags(); ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. #### Kotlin ```kotlin Airship.channel.editTagGroups { addTag("loyalty", "bronze-member") removeTag("loyalty", "bronze-member") setTag("games", "bingo") } ``` #### Java ```java Airship.getChannel().editTagGroups() .addTag("loyalty", "bronze-member") .removeTag("loyalty", "bronze-member") .setTag("games", "bingo") .apply(); ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. #### Kotlin ```kotlin Airship.contact.editTagGroups { addTag("loyalty", "bronze-member") removeTag("loyalty", "bronze-member") setTag("games", "bingo") } ``` #### Java ```java Airship.getContact().editTagGroups() .addTag("loyalty", "bronze-member") .removeTag("loyalty", "bronze-member") .setTag("games", "bingo") .apply(); ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. #### Kotlin ```kotlin Airship.channel.editAttributes { setAttribute("device_name", "Bobby's Phone") setAttribute("average_rating", 4.99) removeAttribute("vip_status") } ``` #### Java ```java Airship.getChannel().editAttributes() .setAttribute("device_name", "Bobby's Phone") .setAttribute("average_rating", 4.99) .removeAttribute("vip_status") .apply(); ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. #### Kotlin ```kotlin Airship.contact.editAttributes { setAttribute("first_name", "Bobby") setAttribute("birthday", Date(524300400000)) } ``` #### Java ```java Airship.getContact().editAttributes() .setAttribute("first_name", "Bobby") .setAttribute("birthday", Date(524300400000)) .apply(); ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. You can set and remove JSON Attributes on a Channel or a Contact. #### Kotlin ```kotlin Airship.contact.editAttributes { setAttribute( attribute = "attribute_name", instanceId = "instance_id", expiration = Date(), json = jsonMapOf( "key" to "value", "another_key" to "another_value" ) ) } ``` ## Verifying Attributes To verify that attributes are set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. Search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. #### Kotlin ```kotlin // Modifying channel subscription lists Airship.channel.editSubscriptionLists { subscribe("food") unsubscribe("sports") } // Fetching channel subscription lists val channelSubscriptions = Airship.channel.fetchSubscriptionLists() ``` #### Java ```java // Modifying channel subscription lists Airship.getChannel().editSubscriptionLists() .subscribe("food") .unsubscribe("sports") .apply(); // Fetching channel subscription lists PendingResult> channelSubscriptions = Airship.getChannel().fetchSubscriptionListsPendingResult(); ``` ## Contact Subscription Lists Contact subscriptions are set at the user level and require a Channel scope that specifies the types to which the subscription list applies. #### Kotlin ```kotlin // Modifying contact subscription lists Airship.contact.editSubscriptionLists { subscribe("food", "app") unsubscribe("sports", "sms") } // Fetching contact subscription lists val contactSubscriptions = Airship.contact.fetchSubscriptionLists() ``` #### Java ```java // Modifying contact subscription lists Airship.getContact().editSubscriptionLists() .subscribe("food", "app") .unsubscribe("sports", "sms") .apply(); // Fetching contact subscription lists PendingResult>> contactSubscriptions = Airship.getContact().fetchSubscriptionListsPendingResult(); ``` ## Verifying Subscription Lists To verify that subscription lists are set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. Search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ### Data Collection Overview of data collection and controls provided by the Airship SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: #### Kotlin | Privacy Manager Flag | Kotlin Constant | AirshipConfig Value | |----------------------|--------------------------------------------|---------------------| | Push | PrivacyManager.Feature.PUSH | push | | In-App Automation | PrivacyManager.Feature.IN_APP_AUTOMATION | in_app_automation | | Message Center | PrivacyManager.Feature.MESSAGE_CENTER | message_center | | Tags and Attributes | PrivacyManager.Feature.TAGS_AND_ATTRIBUTES | tags_and_attributes | | Contacts | PrivacyManager.Feature.CONTACTS | contacts | | Feature Flags | PrivacyManager.Feature.FEATURE_FLAGS | feature_flags | | Analytics | PrivacyManager.Feature.ANALYTICS | analytics | | All | PrivacyManager.Feature.ALL | all | | None | PrivacyManager.Feature.NONE | none | #### Java | Privacy Manager Flag | Java Constant | AirshipConfig Value | |----------------------|--------------------------------------------|---------------------| | Push | PrivacyManager.Feature.PUSH | push | | In-App Automation | PrivacyManager.Feature.IN_APP_AUTOMATION | in_app_automation | | Message Center | PrivacyManager.Feature.MESSAGE_CENTER | message_center | | Tags and Attributes | PrivacyManager.Feature.TAGS_AND_ATTRIBUTES | tags_and_attributes | | Contacts | PrivacyManager.Feature.CONTACTS | contacts | | Feature Flags | PrivacyManager.Feature.FEATURE_FLAGS | feature_flags | | Analytics | PrivacyManager.Feature.ANALYTICS | analytics | | All | PrivacyManager.Feature.ALL | all | | None | PrivacyManager.Feature.NONE | none | ## Configuring default enabled features Default enabled features can be set in the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring `AirshipConfigOptions`, see [Android SDK Setup](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/). #### Kotlin **AirshipConfigOptions** ```kotlin airshipConfigOptions { // ... setEnabledFeatures(PrivacyManager.Feature.PUSH) } ``` **airshipconfig.properties** ```properties enabledFeatures = push ``` #### Java **AirshipConfigOptions** ```java AirshipConfigOptions.newBuilder() // ... .setEnabledFeatures(PrivacyManager.Feature.PUSH) .build(); ``` **airshipconfig.properties** ```properties enabledFeatures = push ``` To fully disable data collection by default, set the enabled features to none. #### Kotlin **AirshipConfigOptions** ```kotlin airshipConfigOptions { // ... setEnabledFeatures(PrivacyManager.Feature.NONE) } ``` **airshipconfig.properties** ```properties enabledFeatures = none ``` #### Java **AirshipConfigOptions** ```java AirshipConfigOptions.newBuilder() // ... .setEnabledFeatures(PrivacyManager.Feature.NONE) .build(); ``` **airshipconfig.properties** ```properties enabledFeatures = none ``` ## Enabling features at runtime You can enable or disable features at runtime based on user consent: #### Kotlin ```kotlin // Initially disable all features val options = airshipConfigOptions { // ... setEnabledFeatures(PrivacyManager.Feature.NONE) } // Later, when user grants consent: Airship.privacyManager.enableFeatures( PrivacyManager.Feature.PUSH, PrivacyManager.Feature.ANALYTICS ) ``` #### Java ```java // Initially disable all features AirshipConfigOptions options = AirshipConfigOptions.newBuilder() // ... .setEnabledFeatures(PrivacyManager.Feature.NONE) .build(); // Later, when user grants consent: Airship.getPrivacyManager().enableFeatures( PrivacyManager.Feature.PUSH, PrivacyManager.Feature.ANALYTICS ); ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Google Play Data Safety](https://www.airship.com/docs/reference/data-collection/google-play-data-safety/) - Reference for Google Play's Data Safety section - [Analytics](https://www.airship.com/docs/developer/sdk-integration/android/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers # Permission Prompts > Request additional system permissions (e.g., location) from users using Opt-in Actions. The Airship SDK automatically handles push notification permissions. For additional permissions like location, you can use Opt-in Actions to prompt users using native permission prompts. Opt-in Actions are a special type of [Action](https://www.airship.com/docs/reference/glossary/#action) that are handled by `PermissionsManager`. For an overview of all supported actions and where they are available, see the [Actions](https://www.airship.com/docs/guides/messaging/messages/actions/) guide. ## Supported Opt-in Types * Push — Handled automatically by the SDK (no implementation needed) * Location — Requires implementing a custom `PermissionDelegate` ## Implementing Location Opt-in To implement Location Opt-in, create a custom `PermissionDelegate` and register it with `PermissionsManager` to handle location permissions. ### Create a Location Permission Delegate #### Kotlin ```kotlin val delegate = SinglePermissionDelegate(Manifest.permission.ACCESS_COARSE_LOCATION) ``` For fine location, use `Manifest.permission.ACCESS_FINE_LOCATION` instead. #### Java ```java SinglePermissionDelegate delegate = new SinglePermissionDelegate(Manifest.permission.ACCESS_COARSE_LOCATION); ``` For fine location, use `Manifest.permission.ACCESS_FINE_LOCATION` instead. ### Register the Permission Delegate After creating a location `PermissionDelegate`, register it with `PermissionsManager` [after Airship is ready](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#customizing-airship): #### Kotlin ```kotlin Airship.permissionsManager .setPermissionDelegate(Permission.LOCATION, delegate) ``` #### Java ```java Airship.getPermissionsManager() .setPermissionDelegate(Permission.LOCATION, delegate); ``` ### Troubleshooting Common issues and solutions for Airship SDK setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/) or [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/android/installation/advanced-integration/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Initialization errors The Airship SDK fails during initialization or behaves unexpectedly in these cases: - `takeOff` has already been successfully called. - `takeOff` was called but the SDK was not configured through Autopilot or `airshipconfig.properties`. - Autopilot or `airshipconfig.properties` has invalid or missing credentials. - Required dependencies are missing from the `build.gradle` file. - `takeOff` was called before the application context was available. ## takeOff called multiple times If `takeOff` throws because it has already been successfully called, verify the following: - `takeOff` is only called once per app launch. - It's not called in both `Autopilot` and `Application.onCreate()`. ## Credential validation During initialization, the SDK checks only that credentials are present and correctly formatted in code, through `Autopilot`, or in `airshipconfig.properties`. It does not verify that the credentials are valid against Airship servers. If the configuration is valid and you initialize the SDK only once, initialization can complete without reporting an error even when the credentials themselves are invalid. The credentials the SDK uses at initialization, whether you call `takeOff` yourself or configure the SDK through `Autopilot` or `airshipconfig.properties`, are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. **Symptoms of missing or invalid credentials:** - No channel ID appears in the logs - Warnings or errors in the logs after initialization - Channel is not created in the Airship dashboard - Push notifications are not received If you are experiencing credential issues, do the following: 1. Compare your Airship project credentials with the values in your app, either in code, through `Autopilot`, or in `airshipconfig.properties`. - Credentials must match the expected format and character set. - Credentials must not be empty strings or contain extra whitespace. 1. Ensure both `productionAppKey`/`productionAppSecret` and `developmentAppKey`/`developmentAppSecret` are set in code, through `Autopilot`, or in `airshipconfig.properties` before the SDK initializes. **Guidelines for credentials:** - Use development credentials for development builds and production credentials for release builds. - Configure both development and production credentials in your app, either in code, through `Autopilot`, or in `airshipconfig.properties`. The SDK chooses which to use based on your build configuration. ## airshipconfig.properties not found or invalid If the SDK cannot load or parse `airshipconfig.properties`, or the file is invalid, verify the following: - The file exists in your app's `src/main/assets/` directory - The file name is exactly `airshipconfig.properties` - All required keys are present: `productionAppKey`, `productionAppSecret`, `developmentAppKey`, `developmentAppSecret` - The file format is valid (it must be in standard Java properties format) - The file is included in your build output ## FCM configuration issues If push notifications are not working or you need to confirm your FCM integration, verify the following: - The `google-services.json` file is configured for your app and Firebase project - The FCM dependency is included in your `build.gradle` file - The Google Services plugin is applied in your `build.gradle` file - The Firebase project is set up correctly in the Firebase Console # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. The SDK provides detailed information about their current state. ## Get Current Notification Status Read the current notification status from `Airship.push.pushNotificationStatus` to inspect each field: #### Kotlin ```kotlin val status = Airship.push.pushNotificationStatus Log.d("Airship", "User notifications enabled: ${status.isUserNotificationsEnabled}") Log.d("Airship", "Notifications allowed: ${status.areNotificationsAllowed}") Log.d("Airship", "Privacy feature enabled: ${status.isPushPrivacyFeatureEnabled}") Log.d("Airship", "Push token registered: ${status.isPushTokenRegistered}") Log.d("Airship", "User opted in: ${status.isUserOptedIn}") Log.d("Airship", "Fully opted in: ${status.isOptIn}") ``` #### Java ```java PushNotificationStatus status = Airship.getPush().getPushNotificationStatus(); Log.d("Airship", "User notifications enabled: " + status.isUserNotificationsEnabled()); Log.d("Airship", "Notifications allowed: " + status.getAreNotificationsAllowed()); Log.d("Airship", "Privacy feature enabled: " + status.isPushPrivacyFeatureEnabled()); Log.d("Airship", "Push token registered: " + status.isPushTokenRegistered()); Log.d("Airship", "User opted in: " + status.isUserOptedIn()); Log.d("Airship", "Fully opted in: " + status.isOptIn()); ``` ## Listen for Status Changes Use the following to monitor notification status changes in real time: #### Kotlin ```kotlin scope.launch { Airship.push.pushNotificationStatusFlow.collect { status -> Log.d("Airship", "Notification status changed:") Log.d("Airship", "User opted in: ${status.isUserOptedIn}") Log.d("Airship", "Fully opted in: ${status.isOptIn}") } } ``` #### Java ```java Airship.getPush().addNotificationStatusListener(status -> { Log.d("Airship", "Notification status changed:"); Log.d("Airship", "User opted in: " + status.isUserOptedIn()); Log.d("Airship", "Fully opted in: " + status.isOptIn()); }); ``` ## Understanding Notification Status Fields The `NotificationStatus` class provides detailed information about why push might not be working: | Field | Description | |-------|-------------| | `isUserNotificationsEnabled` | Whether `pushManager.userNotificationsEnabled` is set to `true` | | `areNotificationsAllowed` | Whether the user has granted notification permissions | | `isPushPrivacyFeatureEnabled` | Whether the push privacy feature is enabled in `PrivacyManager` | | `isPushTokenRegistered` | Whether a push token has been successfully registered with FCM | | `isUserOptedIn` | `true` if user notifications are enabled, privacy feature is enabled, and notifications are allowed | | `isOptIn` | `true` if `isUserOptedIn` is `true` AND a push token is registered | ## Common Status Scenarios **Status:** `isUserNotificationsEnabled = false` - **Cause:** `pushManager.userNotificationsEnabled` has not been set to `true`. - **Solution:** Enable user notifications in your app code. **Status:** `areNotificationsAllowed = false` - **Cause:** User denied notification permissions or permissions not yet requested. (Android 13+) - **Solution:** Request notification permissions or guide user to system settings. **Status:** `isPushPrivacyFeatureEnabled = false` - **Cause:** Push privacy feature is disabled in Privacy Manager. - **Solution:** Enable the push privacy feature: `UAirship.shared().privacyManager.setEnabledFeatures(PrivacyManager.Feature.PUSH)`. **Status:** `isPushTokenRegistered = false` - **Cause:** Device hasn't received a push token from FCM yet. - **Solution:** Check network connectivity, FCM configuration, and device/emulator limitations. **Status:** `isUserOptedIn = true` but `isOptedIn = false` - **Cause:** Push token registration is pending or failed. - **Solution:** Check Logcat for FCM registration errors, verify network connectivity, and ensure proper FCM setup. ## Apple Integrate the Apple Airship SDK into your mobile applications for iOS, tvOS, and visionOS. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/embedding/#handling-display-requests) and [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/apple/preference-center/embedding/#handling-display-requests). #### Swift Set the deep link listener [after takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/): ```swift // Set deep link callback Airship.onDeepLink = { url in // Handle deep link asynchronously await someNavigationTask(url) } ``` #### Objective-C Conform to `UADeepLinkDelegate` and set the delegate [after takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/): ```objc @interface AppDelegate() @end @implementation AppDelegate // ... in didFinishLaunchingWithOptions, after takeOff ... UAirship.deepLinkDelegate = self; - (void)receivedDeepLink:(NSURL *_Nonnull)deepLink completionHandler:(void (^_Nonnull)(void))completionHandler { // Handle deep link completionHandler(); } @end ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. In iOS, actions are sent as part of the notification payload as top-level key values, where the key is the action name and the value is the action's argument (any valid JSON value). For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Action Situations Actions are triggered with extra context in the form of a Situation. The different situations allows actions to determine if they should run or not, and possibly do different behavior depending on the situation. | Description | iOS | |-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| | Action was invoked manually. | [manualInvocation](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/manualinvocation) | | Action was invoked from a launched push notification. | [launchedFromPush](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/launchedfrompush) | | Action was invoked from a received push notification in the foreground. | [foregroundPush](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/foregroundpush) | | Action was invoked from a received push notification in the background. | [backgroundPush](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/backgroundpush) | | Action was invoked from JavaScript or a URL. | [webViewInvocation](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/webviewinvocation) | | Action was invoked from a foreground interactive notification button. | [foregroundInteractiveButton](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/foregroundinteractivebutton) | | Action was invoked from a background interactive notification button. | [backgroundInteractiveButton](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/backgroundinteractivebutton) | | Action was invoked from automation. | [automation](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/automation) | ## Action Registry The action registry is the central place to register actions by name. Each entry in the registry contains an action, the names that the action is registered under, a predicate that allows filtering when an action should run, and allows specifying alternative actions for different situations. #### Swift ```swift Airship.actionRegistry.registerEntry( names: ["action_name", "action_alias"], ) { return ActionEntry(action: action) } ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. #### Swift ```swift let entry = Airship.actionRegistry.entry(name: "action_name") ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. #### Swift ```swift // Predicate that only allows the action to run if it was launched from a push let predicate: @Sendable (ActionArguments) async -> Bool = { args in return args.situation == .launchedFromPush } // Update the predicate Airship.actionRegistry.updateEntry(name: "action_name", predicate: predicate) ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. ## Triggering Actions In addition to triggering an action from a message, they can be programmatically triggered as well. #### Swift ```swift let result = await ActionRunner.run( actionName: "action_name", arguments: ActionArguments( string: "some value", situation: .manualInvocation ) ) // Run an action directly let result = await ActionRunner.run( action: action, arguments: ActionArguments( string: "some value", situation: .manualInvocation ) ) ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. ## Custom Actions The action framework supports any custom actions. Create an action by extending the `Action` protocol on iOS. iOS also allows actions to be defined using blocks. After `takeoff`, register the action. The action can be triggered the same way as built-in actions. #### Swift ```swift let customAction = BlockAction { args in print("Action is performing with args: \(args)") return nil } Airship.actionRegistry.registerEntry(names: ["custom_action"]) { return ActionEntry(action: customAction) } ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. The SDK provides asynchronous access to feature flags using an async method, which are intended to be called from a Task or a function that supports concurrency. For more information, see [Concurrency guide](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/). ```swift // Get the FeatureFlag let flag: FeatureFlag = try? await Airship.featureFlagManager.flag(name: "YOUR_FLAG_NAME") // Check if the app is eligible or not if (flag?.isEligible == true) { // Do something with the flag } else { // Disable feature or use default behavior } ``` > **Note:** This feature is not supported in Objective-C. ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/apple/data-collection/privacy-manager/). ```swift Airship.featureFlagManager.trackInteraction(flag: featureFlag) ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. ```swift do { let flag = try await Airship.featureFlagManager.flag(name: "YOUR_FLAG_NAME") if (flag.isEligible == true) { // Do something with the flag } } catch { // Do something with the error } ``` # Live Activities > Integrate Live Activities into your iOS app to display real-time updates on the Lock Screen and Dynamic Island. {{< badge "axp" >}} For the push API method, see the [iOS Live Activities](https://www.airship.com/docs/guides/messaging/features/ios-live-activities/) messaging guide. See also the [iOS Live Activities](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ## App Setup To support Live Activities, you must call `restoreLiveActivityTracking` *once* after `takeOff` with all the Live Activity types that you might track with Airship. This allows Airship to resume tracking any previously tracked activities across app inits and to automatically track the `pushToStartToken` that allows starting activities through a push notification. #### Swift ```swift Airship.takeOff(config, launchOptions: launchOptions) Airship.channel.restoreLiveActivityTracking { restorer in await restorer.restore(forType: Activity.self) await restorer.restore(forType: Activity.self) } ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. After the `restore` call above, Airship will track the `pushToStartTokens` for the activity's attribute types. You can then start a Live Activity through a push notification. Starting a Live Activity does not automatically track it. Instead, the app will be woken up and you must call through to Airship with the activity instance and the name. ### Watching for Live Activities There is no entry point into the app when it is started for a Live Activity being created. Instead, you need to query Live Activities on init and when a `pushToStartToken` update is received to track them through Airship. Airship provides an extension `Activity.airshipWatchActivities(activityBlock:)` that can be used to do this for you. In this example, we assume the `gameID` on our `SportsActivityAttributes` will be used to send updates through Airship after it is created: #### Swift ```swift Airship.channel.restoreLiveActivityTracking { restorer in await restorer.restore(forType: Activity.self) } Activity.airshipWatchActivities { activity in Airship.channel.trackLiveActivity(activity, name: activity.attributes.gameID) } ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. ## Starting Live Activities To start a Live Activity from the app, make sure to set the `pushType` to `.token`. After it is started, immediately track it with `Airship.channel.trackLiveActivity(_:name:)`. #### Swift ```swift let activity = try Activity.request( attributes: attributes, content: content, pushType: .token ) Airship.channel.trackLiveActivity( activity, name: attributes.gameID ) ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. ## Updating Live Activities To update a Live Activity, use the standard ActivityKit APIs. First find the Activity instance then call `update` on it: #### Swift ```swift guard let activity = Activity.activities.first(where: { $0.id == "sports-game-123" }) else { // not found return } activity.update(contentUpdate) ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. ## Ending Live Activities To end a Live Activity, use the standard ActivityKit APIs. First find the Activity instance then call `end` on it with a dismissal policy: #### Swift ```swift guard let activity = Activity.activities.first(where: { $0.id == "sports-game-123" }) else { // not found return } activity.end(contentUpdate, dismissalPolicy: .default) ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. Analytics allows you to track user engagement and app performance through custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/apple/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with [Custom Events](https://www.airship.com/docs/reference/glossary/#custom_event). They require enabling analytics for your app. #### Swift ```swift var event = CustomEvent(name: "event_name", value: 123.12) try event.setProperties( [ "my_custom_property": "some custom value", "is_neat": true, "any_json": [ "foo": "bar" ] ] ) event.track() ``` #### Objective-C ```objc UACustomEvent *event = [[UACustomEvent alloc] initWithName:@"event_name" value:123.12]; [event setProperties: @{ @"my_custom_property": @"some custom value", @"is_neat": @YES, @"any_json": @{ @"foo": @"bar" } } error:&error]; [event track]; ``` ### Templates Custom Event Templates are a wrapper for Custom Events and are available for iOS, [Android](https://www.airship.com/docs/developer/sdk-integration/android/analytics/#templates), and [Web](https://www.airship.com/docs/developer/sdk-integration/web/analytics-and-reporting/#templates). See also [CustomEvent](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/customevent) in the iOS SDK library. #### Account Use this template to create Custom Events for account-related events. The template is written with account registration as the example. #### Swift Track a registered account event: ```swift let acctEvent = CustomEvent(accountTemplate: .registered) acctEvent.track() ``` With optional properties: ```swift var acctEvent = CustomEvent( accountTemplate: .registered, properties: CustomEvent.AccountProperties( category: "Premium", isLTV: true ) ) acctEvent.eventValue = 9.99 acctEvent.transactionID = "12345" acctEvent.track() ``` #### Media Use this template to create Custom Events for media-related events, including consuming, browsing, starring, and sharing content. #### Swift Track a consumed content event: ```swift let mediaEvent = CustomEvent(mediaTemplate: .consumed) mediaEvent.track() ``` With an optional value: ```swift var mediaEvent = CustomEvent( mediaTemplate: .consumed, properties: CustomEvent.MediaProperties(isLTV: true) ) mediaEvent.eventValue = 1.99 mediaEvent.track() ``` With optional properties: ```swift var mediaEvent = CustomEvent( mediaTemplate: .consumed, properties: CustomEvent.MediaProperties( id: "12322", category: "entertainment", type: "video", eventDescription: "Watching latest entertainment news.", author: "UA Enterprises", isFeature: true, isLTV: true ) ) mediaEvent.eventValue = 2.99 mediaEvent.track() ``` #### Swift Track a starred content event: ```swift let mediaEvent = CustomEvent(mediaTemplate: .starred) mediaEvent.track() ``` With optional properties: ```swift var mediaEvent = CustomEvent( mediaTemplate: .starred, properties: CustomEvent.MediaProperties( id: "12322", category: "entertainment", type: "video", eventDescription: "Watching latest entertainment news.", author: "UA Enterprises", isFeature: true ) ) mediaEvent.eventValue = 2.99 mediaEvent.track() ``` #### Swift Track a browsed content event: ```swift let mediaEvent = CustomEvent(mediaTemplate: .browsed) mediaEvent.track() ``` With optional properties: ```swift let mediaEvent = CustomEvent( mediaTemplate: .browsed, properties: CustomEvent.MediaProperties( id: "12322", category: "entertainment", type: "video", eventDescription: "Browsed latest entertainment news.", author: "UA Enterprises", isFeature: true ) ) mediaEvent.track() ``` #### Swift Track a shared content event: ```swift let mediaEvent = CustomEvent(mediaTemplate: .shared) mediaEvent.track() ``` With a source and medium: ```swift let mediaEvent = CustomEvent( mediaTemplate: .shared(source: "facebook", medium: "social") ) mediaEvent.track() ``` With optional properties: ```swift var mediaEvent = CustomEvent( mediaTemplate: .shared(source: "facebook", medium: "social"), properties: CustomEvent.MediaProperties( id: "1234", category: "entertainment", type: "video", eventDescription: "Watching latest entertainment news.", author: "UA Enterprises", isFeature: true ) ) mediaEvent.track() ``` #### Retail Use this template to create Custom Events for retail-related events, including browsing a product, adding an item to a cart, purchasing an item, starring a product, and sharing a product. #### Swift Track a purchased event: ```swift let retailEvent = CustomEvent(retailTemplate: .purchased) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .purchased, properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", isLTV: true, brand: "SpecialBrand", isNewItem: true ) ) retailEvent.eventValue = 99.99 retailEvent.transactionID = "13579" retailEvent.track() ``` #### Swift Track a browsed event: ```swift let retailEvent = CustomEvent(retailTemplate: .browsed) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .browsed, properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", brand: "SpecialBrand", isNewItem: true ) ) retailEvent.eventValue = 99.99 retailEvent.transactionID = "13579" retailEvent.track() ``` #### Swift Track an added-to-cart event: ```swift let retailEvent = CustomEvent(retailTemplate: .addedToCart) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .addedToCart, properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", brand: "SpecialBrand", isNewItem: true ) ) retailEvent.eventValue = 99.99 retailEvent.transactionID = "13579" retailEvent.track() ``` #### Swift Track a starred product event: ```swift let retailEvent = CustomEvent(retailTemplate: .starred) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .starred, properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", brand: "SpecialBrand", isNewItem: true ) ) retailEvent.eventValue = 99.99 retailEvent.transactionID = "13579" retailEvent.track() ``` #### Swift Track a shared product event: ```swift let retailEvent = CustomEvent(retailTemplate: .shared()) retailEvent.track() ``` With a source and medium: ```swift let retailEvent = CustomEvent( retailTemplate: .shared(source: "facebook", medium: "social") ) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .shared(source: "facebook", medium: "social"), properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", brand: "SpecialBrand", isNewItem: true ) ) retailEvent.transactionID = "13579" retailEvent.track() ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. #### Swift ```swift let identifiers = Airship.analytics.currentAssociatedDeviceIdentifiers() identifiers.set(identifier: "value", key:"key") Airship.analytics.associateDeviceIdentifiers(identifiers) ``` #### Objective-C ```objc UAAssociatedIdentifiers *identifiers = [UAirship.analytics currentAssociatedDeviceIdentifiers]; [identifiers setIdentifier:@"value" forKey:@"key"]; [UAirship.analytics associateDeviceIdentifiers:identifiers]; ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. #### Swift ```swift Airship.analytics.trackScreen("MainScreen") ``` #### Objective-C ```objc [UAirship.analytics trackScreen:@"MainScreen"]; ``` # Apple SDK Changelog > The latest updates to the Airship iOS SDK. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 20.7.0 April 30, 2026 Minor release that adds support for Native Message Center and fixes Firebase coexistence for reliable push handling. ### Changes - Added support for rendering Native Content in Message Center - Fixed Firebase coexistence to ensure reliable push handling ## 20.6.3 April 24, 2026 Patch release with Scenes reliability fixes and a VoiceOver accessibility fix for paged scenes. ### Changes - Fixed form submit ordering so the form status is marked submitted before the onSubmit callback runs - Fixed pager summary events to include the correct layout context on dismiss and reflect pager completion - Fixed VoiceOver so dismiss and navigation buttons remain reachable on paged scenes ## 19.11.8 April 21, 2026 Patch release that fixes Xcode 26.4 build issues with whole module optimization. ### Changes - Fixed Xcode 26.4 build issues with whole module optimization. ## 20.6.2 April 2, 2026 Patch release that improves border rendering in Scenes. ### Changes - Support per-corner border for shapes drawn in Scenes ## 19.11.7 March 31, 2026 Patch release that improves pager navigation reliability in Scenes. ### Changes - Improved pager navigation reliability by fixing a race condition that could cause desync when swiping rapidly during scroll. ## 20.6.0 March 21, 2026 Minor release that adds Objective-C wrappers for the Message Center native bridge to support .NET bindings, adds subscript and superscript text support in Scenes, and improves Message Center reliability. ### Changes - Added `UAMessageCenterNativeBridge` and `UAMessageCenterNativeBridgeDelegate` to support .NET MAUI bindings for Message Center web view deep link handling. - Added subscript and superscript text support in Scenes. - Fixed Message Center mark-as-read not updating the list UI. - Improved Message Center content type handling. - Improved Message Center behavior when a message is unavailable or deleted. ## 20.5.0 March 12, 2026 Minor release that improves video playback and improves pager navigation reliability in Scenes. ### Changes - Improved pager navigation reliability by fixing a race condition that could cause desync when swiping rapidly during scroll. - Improved NPS score selector tap targets to remain consistent when toggling between selected and unselected states. - Improved video playback lifecycle handling to prevent potential memory leaks on dismiss. - Improved In-App Automation schedule parsing to retry failed schedules when remote data is updated. - Removed remaining Objective-C from AirshipBasement. - Replaced Objective-C based swizzling with a more reliable Swift implementation. - Fixed video aspect ratio to avoid cropping content when using center-inside display mode. ## 20.4.0 February 25, 2026 Minor release that adds support for Native Message Center. Native content type requires displaying the message content in a `MessageCenterMessageView`. Apps that do not use Airship's message views (e.g. using a WebView directly) should filter out messages where `message.contentType` is not `.html`. ### Changes - Added support for Native Message Center. - Removed gzip encoding using internal UACompression class and `libz` dependency from AirshipBasement. - Added `ExpressibleBy` protocol conformances to `AirshipJSON` allowing initialization with literals. - Added `UAEmbeddedViewControllerFactory` to the `AirshipObjectiveC` module for embedding `AirshipEmbeddedView` in Objective-C applications. - Improved accessibility for single choice and multiple choice questions in Scenes. ## 20.3.2 February 24, 2026 Patch release that fixes Message Center unread indicator behavior, improves spinner fallback on older iOS versions, and resolves visionOS availability checks. ### Changes - Fixed Message Center unread indicator rendering so the unread indicator is only shown for unread messages. - Fixed spinner behavior on iOS versions earlier than 18 by adding a rotating fallback icon. - Fixed visionOS compilation and availability handling for newer iOS and visionOS APIs. ## 20.3.1 February 19, 2026 Patch release that fixes an Xcode 26.4 beta compilation issue and improves Scene stability. ### Changes - Fixed an Xcode 26.4 beta compilation issue. - Improved Scene stability by adding guards for non-finite layout values used in SwiftUI frame, offset, and position calculations. - Added additional Scene layout safety checks for container, pager, story indicator, video controls, and wrapping layout views. - Improved pager timer progression by safely handling zero and invalid page delays. ## 19.11.6 February 18, 2026 Patch release that fixes an Xcode 26.4 beta compilation issue and improves Scene stability. ### Changes - Fixed an Xcode 26.4 beta compilation issue. - Improved Scene stability by adding guards for non-finite layout values used in SwiftUI frame, offset, and position calculations. - Added additional Scene layout safety checks for container, pager, story indicator, video controls, and wrapping layout views. - Improved pager timer progression by safely handling zero and invalid page delays. ## 20.3.0 January 30, 2026 Minor release that adds Objective-C wrapper for deep link processing and fixes a Message Center migration issue when upgrading from 17.x or older to 20.x. ### Changes - Added `UAirship.processDeepLink(_:completionHandler:)` Objective-C wrapper for programmatic deep link handling. - Fixed a Message Center migration issue when upgrading from 17.x or older to 20.x ## 20.2.0 January 30, 2026 Minor release that adds Objective-C wrappers for .NET bindings and improves bundle resource lookup for SPM and Tuist projects. ### Changes - Added Objective-C wrappers for permissions manager and push notification status APIs to support .NET bindings. - Improved bundle resource lookup to better support Swift Package Manager and Tuist generated projects. - Fixed page tabbing, radio buttons, and checkbox accessibility in Scenes. - Improved banner IAA message accessibility. ## 20.1.1 January 16, 2026 Patch release that improves VoiceOver focus control and sizing for the progress bar indicator in Story views. ### Changes - Added support for sizing inactive segments in Story view progress indicators. - Improved VoiceOver focus handling for Message Center Web content. ## 20.1.0 January 10, 2026 Minor release that includes several fixes and improvements for Scenes, In-App Automations, and Message Center. ### Changes - Added support for Story pause/resume and back/next controls. - Added Scenes content support in Message Center. - Added support for additional text styles in Scenes. - In-App Automations and Scenes that were not available during app launch can now be triggered by events that happened in the previous 30 seconds. - Fixed pinned container background image scaling when the keyboard is visible in Scenes. - Fixed banner presentation and layout in Scenes. - Fixed progress icon rendering in Scenes. - Fixed container layout alignment in Scenes. ## 20.0.3 December 18, 2025 Patch release that fixes keyboard safe area with Scenes. ### Changes - Fixed safe area handling during keyboard presentation in Scenes. ## 19.11.5 December 18, 2025 Patch release that fixes keyboard safe area with Scenes. ### Changes - Fixed safe area handling during keyboard presentation in Scenes. ## 20.0.2 November 25, 2025 Patch release that fixes an issue with delayed video playback in Scenes when initially loading or paging and addresses a direct open attribution race condition which could cause direct open events to be missed in some edge cases. ### Changes - Fixed an issue where the video ready callback was not assigned before observers were set up, causing the pager to miss the ready signal and advance before they loaded completely. - Fixed a potential race condition that could result in missed direct open attributions by ensuring notification response handling completes synchronously before the app becomes active. ## 19.11.4 November 25, 2025 Patch release that fixes an issue with delayed video playback in Scenes when initially loading or paging. Applications that use videos in Scenes must update to resolve the playback delays. ### Changes - Fixed an issue where the video ready callback was not assigned before observers were set up, causing the pager to miss the ready signal and advance before the loaded completely. ## 19.11.3 November 19, 2025 Patch release that further addresses the direct open attribution race condition introduced in 19.0.0. While 19.11.0 attempted to fix this issue by introducing a synchronous completion handler method, the implementation still executed work asynchronously, which could cause direct open events to be missed in some edge cases. ### Changes - Fixed a potential race condition that could result in missed direct open attributions by ensuring notification response handling completes synchronously before the app becomes active. ## 19.11.2 November 14, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Fixed YouTube video embedding to comply with YouTube API Client identification requirements. ## 20.0.1 November 14, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Fixed looping behavior in video views within Scenes. - Fixed Message Center icon display when icons are enabled. - Fixed pager indicator accessibility to prevent duplicate VoiceOver announcements. - Added dismiss action to banner in-app messages for improved VoiceOver accessibility. - Fixed YouTube video embedding to comply with YouTube API Client identification requirements. ## 20.0.0 October 9, 2025 Major SDK release with several breaking changes. See the [Migration Guide](https://github.com/urbanairship/ios-library/blob/main/Documentation/Migration/migration-guide-19-20.md) for more info. ### Changes - Xcode 26+ is now required. - Updated minimum deployment target to iOS 16+. - Refactored Message Center and Preference Center UI to provide clearer separation between navigation and content views. See the migration guide for API changes. - Introduced modern, block-based and async APIs as alternatives to common delegate protocols (`PushNotificationDelegate`, `DeepLinkDelegate`, etc.). The delegate pattern is still supported but will be deprecated in a future release. - Refactored core Airship components to use protocols instead of concrete classes, improving testability and modularity. See the migration guide for protocol renames and class-to-protocol conversions. - Added support for split view in the Message Center, improving the layout on larger devices. - Updated the Preference Center with a refreshed design and fixed UI issues on tvOS and visionOS. - Fixed Package.swift to remove macOS as a supported platform. - CustomViews within a Scene can now programmatically control their parent Scene, enabling more dynamic and interactive custom content. - Accessibility updates for Scenes. - New AirshipDebug package that exposes insights and debugging capabilities into the Airship SDK for development builds, providing enhanced visibility into SDK behavior and performance. - Removed automatic collection of connection_type and carrier device properties ## 19.11.1 October 7, 2025 Patch release addressing the longstanding Swift concurrency crash (GH-434) and improving the internal rate-limiting system for better stability and efficiency. ### Changes - Refactored WorkRateLimiter to improve efficiency and reliability, reduce memory overhead, and eliminate unnecessary temporary allocations. - Added stronger safeguards to WorkRateLimiter prevent rare edge-case crashes in rate-limiting logic. ## 19.11.0 October 1, 2025 This is an important update for apps using manual push notification integration (automaticSetup = false). We are addressing a lifecycle issue caused by Apple's async notification delegate being called on a background thread, unlike the main-thread-guaranteed completionHandler version. To align with the correct lifecycle, we are deprecating our async handler and introducing a new completionHandler method. Using the async version can cause direct open counts to be lower than expected. ### Changes - Added a new synchronous `AppIntegration.userNotificationCenter(_:didReceive:withCompletionHandler:)` method. Apps must use this and the corresponding synchronous delegate method to ensure notification responses are handled before the app becomes active. - The async `AppIntegration.userNotificationCenter(_:didReceive:)` method is now deprecated. - Landing pages no longer display for push notifications received when the app is in the foreground. ## 19.10.0 September 22, 2025 Minor release that adds a new flag to work around the critical crash (GH-434) affecting Swift 5 apps on Xcode 16.1+. The problematic feature is now disabled by default. ### Changes - Added `isDynamicBackgroundWaitTimeEnabled` flag. This defaults to `false` to avoid the crash. It is strongly recommended to keep this `false` for Swift 5 apps. Swift 6 apps can safely set this to `true` to restore previous behaviors. ## 19.9.2 September 16, 2025 Patch release that resolves a crash eminating from the Thomas video player, fixes a bug that causes Scenes to sometimes display after being stopped, and fixes some UI bugs exposed by iOS 26. ### Changes - Fixed refreshing out of date In-App Automations and Scenes before displaying. - Fixed KVO in ThomasVideoPlayer to use modern patterns and properly release observers. - Fixed Message Center title bar theming in iOS 26. - Improved tab bar UI in iOS 26. ## 19.9.0 September 4, 2025 Minor release that adds a new flag to HTML In-App message content to force full screen on all devices. ### Changes - Added `forceFullScreen` to HTML In-App message content ## 19.8.3 August 25, 2025 A patch release that includes a targeted fix for the ongoing Swift interoperability crashes outlined in GH-434. ### Changes - Updated the concurrency pattern in the background task scheduler, replacing a `for-await` loop with a `TaskGroup`. This change targets a suspected instability in Swift's concurrency runtime and is expected to mitigate the crashes seen in mixed Swift 5/6 environments. - Fixed a Scene issue where labels marked as H3 were being treated as H1. - Improved accessibility of embedded Scenes by announcing screen changes when an embedded view is displayed. ## 19.8.2 August 19, 2025 A patch release with bug fixes for video in scenes and Swift interoperability crashes. Users that have upgraded to SDK 19.6.0+ and display Youtube or Vimeo videos in Scenes or In-app Messages or are experiencing crashes like those outlined in GH-434 are encouraged to update. ### Changes - Fixed bug preventing proper rendering of youtube and vimeo videos in Scenes and In-app Messages. - Fixed Swift 5-6 interop issues causing crashes in Workers.calculateBackgroundWaitTime(maxTime:) outlined in github issue 434. ## 19.8.1 August 7, 2025 A patch release with improved Xcode 26 support, a fix for custom font scaling in scenes, and internal improvements to image loading. ### Changes - Fixed issues affecting custom font scaling. - Improved image loading support. - Fixed a compilation error caused by SwiftUICore import exposed by Xcode 26 beta 4. ## 19.8.0 July 24, 2025 A minor release with improvements to Scenes and a new `dismiss` command for the JS interface. ### Changes - Added support in Scenes for linking form inputs to a label for better accessibility. - Added container item alignment to Scenes to change the natural alignment within a container. - Added a new `dismiss` command to the JavaScript interface for parity with Android. The new `UAirship.dismiss()` method behaves the same as `UAirship.cancel()`. ## 19.7.0 July 18, 2025 A minor release that simplifies takeOff by deprecating methods with launchOptions, adds flexibility for initialization, and includes several bug fixes. ### Changes - Deprecated `Airship.takeOff` methods that include launchOptions. The takeOff method still needs to be called before `application(_:didFinishLaunchingWithOptions:)` finishes to ensure proper notification delegate is set up. - Updated `Airship.takeOff` to allow it to be called from `MainApp.init` before the application delegate is set, even with automatic setup enabled. - Fixed a stack overflow exception when using Scenes in the iOS 26 beta. - Added a potential workaround for reported crashes within `AirshipWorkManager` and `AirshipChannel`. - Fixed a race condition in Scene asset file operations and improved file management. ## 18.14.4 July 15, 2025 Patch release backported to ensure in-app views are still in the window hierarchy before updating constraints. ### Changes - Fixed crash caused by constraints updating when in-app view isn't in the view hierarchy. ## 19.6.1 June 24, 2025 Patch release with bug fixes for memory management, survey interactions, and accessibility improvements. ### Changes - Fixed a memory issue in `AirshipWorkManager` where temporary arrays were being created unnecessarily when calculating background wait times. - Fixed an issue where NPS survey score selection required double-tapping by properly restoring both the score value and index when loading from form state. - Fixed a potential crash when updating constraints for banner views that have been removed from the view hierarchy. - Improved VoiceOver accessibility by ensuring toggles, checkboxes, and radio inputs remain accessible even without explicit accessibility descriptions. - Added accessibility header traits to section titles in Message Center, Preference Center, and other UI components for better VoiceOver navigation. ## 19.6.0 June 17, 2025 A minor update with enhancements to Scenes and Message Center functionality and a bug fix for Automation. This version is required for Scene branching and phone number collection. ### Changes Automation: - Fixed version trigger predicate matching to properly evaluate app version conditions. Message Center: - Added support to automatically pick up UIKit navigation controller styling. Scenes: - Fixed layout issues with modal frames, specifically related to margins and borders. - Fixed several issues related to Scene branching. - Added support for custom corner radii on borders. - Added support for more flexible survey toggles. ## 19.5.0 May 23, 2025 Minor release focused on performance improvements for Scenes. ### Improvements - Improved load times for Scenes by prefetching assets concurrently. ## 19.4.0 May 15, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. ## 19.3.2 May 8, 2025 Patch release that fixes Message Center listing not refreshing on push received. This issue was introduced in 19.0.0. Apps using Message Center should update. ### Changes - Fixed Message Center behavior on push received. ## 19.3.1 April 28, 2025 Patch release that fixes an issue in a branching scene where a button required two presses to navigate to the next page instead of one. Apps planning on using the upcoming branching feature should update. ### Changes - Fixed Scene button navigation with branching. ## 19.3.0 April 24, 2025 Minor release adding branching and SMS support for Scenes. ### Changes - Added support for branching in Scenes. - Added support for phone number collection and registration in Scenes. - Added `Airship.inAppAutomation.statusUpdates` to track rule update statuses for In-App Automation, Scenes, and Surveys. - Added `Airship.featureFlagManager.statusUpdates` to monitor rule update statuses. - Added support for setting JSON attributes for Channels and Contacts. - Added missing bindings for Obj-C. - Improved accessibility for Banner In-App messages and automations. - Added `TagActionMutation` stream to emit tag updates from `AddTagsAction` and `RemoveTagsAction`. ## 19.2.1 April 17, 2025 Resolved a regression introduced in 19.2.0 where channel audience updates and In-App experiences were unintentionally blocked when the Contact privacy manager flag was disabled. ### Changes - Fixed Channel operations and IAX being blocked when Contacts are disabled. ## 19.2.0 April 3, 2025 Minor release with Custom Views functionality allowing native SwiftUI views to be displayed in Scenes. ### Changes - Added Custom Views functionality allowing native SwiftUI views to be displayed in Scenes. ## 19.1.2 March 31, 2025 Patch release with bug fix for swipe gestures in Scenes. ### Changes - Fixed regression that caused horizontal swipe gestures to be disabled on some devices. ## 19.1.1 March 25, 2025 Patch release with bug fixes and minor improvements. ### Changes - Fixed a bug that allowed channel registration updates to proceed in certain cases when all features were disabled via the Privacy Manager. - Fixed a potential bug involving unecessary comparison checks in the layout system. ## 19.1.0 February 20, 2025 Minor release that adds support for email registration in Scenes, fixes bugs, and improves Airship configuration, Scene keyboard avoidance, and logging. ### Changes - Updated the keyboard avoidance for Scenes to use standard window insets - Added `resolveInProduction()` method on `AirshipConfig` to expose how Airship resolves the `inProduction` flag during takeOff - Added support for email registration in Scenes - Fixed regression with log level check that was introduced in 19.0.0 - Fixed voice over with NPS score for Surveys - Added logger to `UANotificationServiceExtension`. The logger can be configured by overriding the `airshipConfig` property. - Fixed Carthage build failures caused by UIKit Sample project ## 19.0.3 February 4, 2025 Patch release to fix a crash caused by combine subjects being updated from multiple queues. ### Changes - Fixed a crash caused by combine subjects being updated from multiple queues ## 19.0.2 February 3, 2025 Patch release to fix a crash caused by banner size changes during dismissal. ### Changes - Fixed crash caused by banner size changes during dismissal. ## 18.14.3 January 31, 2025 Patch release backported to fix landing page dismiss button default color on iOS and crash caused by banner size changes during dismissal. ### Changes - Updated landing page dismiss button default color on iOS to black. - Fixed crash caused by banner size changes during dismissal. ## 19.0.1 January 30, 2025 Patch release that fixes a crash when the device toggles airplane mode. Apps using 19.0.0 should update. ### Changes - Fixed crash in `WorkConditionsMonitor` when the device toggles airplane mode. - Added `@MainActor` to `RegistrationDelegate` protocol methods. - Updated default dismiss button color from white to black for landing pages to match Android. - Removed top padding on modal and full screen IAAs when using header_media_body and header_body_media without anything above the media. ## 19.0.0 January 16, 2025 Major SDK release with several breaking changes. see the [Migration Guide](https://github.com/urbanairship/ios-library/tree/main/Documentation/Migration/migration-guide-18-19.md) for more info. ### Changes - Xcode 16.2+ is now required. - Updated min versions to iOS 15+ & tvOS 18+. - Migrated all modules to Swift 6. - Objective-C support has been moved into AirshipObjectiveC framework freeing the SDK to expose Swift only APIs. - Updated several APIs to use structs instead of classes. - AppIntegration and PushNotificationDelegate expose async methods instead of completion handlers. - Airship.takeOff can now throw instead of silently failing for better error handling. - New CustomEvent template APIs. - Remove unused NotificationContent extension. - Fixed Scene animation when the device screen orientation changes with auto-height modals. - Added support for wrapping score views in Scenes. - Added support for Preference Center and Feature Flags to tvOS. - Added support for Feature Flag experimentation. ## 18.14.2 January 9, 2025 Patch release to fix extra spacing in a Banner In-App Automations if its missing the heading or body. ### Changes - Fix Banner In-App Automation extra spacing. ## 18.14.1 December 20, 2024 Patch release to fix Banner In-App Automations if the image is taller than the text. ### Changes - Fix Banner In-App Automation sizing issue. ## 18.14.0 December 19, 2024 Minor release that fixes issues with Banner In-App Automations, reduces power usage with In-App Automations & Scenes, and updates how Feature Flags are resolved. ### Changes - Added `resultCache` to `FeatureFlagManager`. This cache is managed by the app and can be optionally used when resolving a flag as a fallback if the flag fails to resolve or if the flag rule set does not exist. - FeatureFlag resolution will now resolve a rule set even if the listing is out of date. - Fixed issue with In-App Automation banners constraints, causing the banner to sometimes steal focus from the underlying app screen or not fully display. - Fixed issue with Surveys that require multi choice or single choice questions not blocking submission. - Reduced the CPU overhead with In-App Automations & Scene execution to reduce overall power usage. ## 18.13.0 December 5, 2024 Minor release that improves a11y support, updated Preference Center UI, and fixes several minor and improvements in Scenes and in-app message banners. ### Changes - Added support for email collection in Scenes - Updated Preference Center UI to use standard padding, titles, and colors to improve the look and feel across different platforms. - Added support to mark a label as a heading in Scenes. - Added support for auto-height modals in Scenes. - Fixed banner duration not dismissing the banner. - Fixed dismissal issues for banners with a height less than 100pts. - Fixed padding issue in bottom-placed in-app banners. ## 18.12.2 November 27, 2024 Patch release that resolves a minor memory-related bug and adds more useful logging around Feature Flag evaluation. ### Changes - Fixed minor memory-related bug that could result in a rare crash. - Improved logging around Feature Flag evaluation. ## 18.12.1 November 6, 2024 Patch release that resolves an issue with Firebase integrations in React Native and Flutter and an issue with opt-in checks when `requestAuthorizationToUseNotifications` is set to false. ### Changes - Fixed issues caused by swizzling conflicts with some Firebase framework integrations. - Fixed opt-in check permissions querying when `requestAuthorizationToUseNotifications` is set to false. ## 18.12.0 November 1, 2024 Minor release with several enhancements to Scenes. ### Changes - Added box shadow support for modal Scenes - Added a new implementation of the Scene pager to lazily load pages on iOS 17+, reducing the overall memory while a Scene is displaying - Added new Scene layout to make anything clickable within a Scene - Added additional logging to deep link handling to make it obvious how the deep link is being processed - Updated border handling on Scenes. Borders are no longer overlaid to avoid issues with borders that are not fully opaque and button borders being overdrawn when tapped - Improved accessibility of scene story indicator. Indicator has been updated to make it obvious which page is active by reducing the height of the inactive pages. Previously this was conveyed only through color - Fixed center_crop scaling in a Scene when a dimension is `auto` but the image is unable to fully fit in the container - Fixed IAA banners drag to dismiss gesture when the gestures starts within a button ## 18.11.1 October 15, 2024 Patch release to avoid implicit unwrap when UINavigationBar appearance tintColor is unset. Applications that use the PreferenceCenter should update. ### Changes - Removes implicit unwrap of the UINavigationBar appearance tintColor. ## 18.11.0 October 12, 2024 Minor release with Message Center and Preference center theming bug fixes and improvements, and a bug fix for IAA videos. Applications that send IAA videos or theme the Message Center or Preference Center and should update. ### Changes - Improved Message Center theming with a focus on improving nagivation components. - Improved Preference Center theming with a focus on improving nagivation components. - Fixed an issue that prevented IAA videos from properly displaying. ## 18.10.0 October 3, 2024 Minor release with accessibility updates, Message Center theming improvements and several bug fixes. ### Changes - Fixed Message Center background color and back button theming. - Fixed tap events in Scenes being registered by their containers in some instances. - Improved accessibility support in Scenes, Message Center and Preference Center with paging actions, localized content descriptions and traits. - Added ability to theme Message Center with a custom style. - Updated webview backgrounds to be clear when displaying media. ## 18.9.2 September 23, 2024 Patch release to fix an issue with high energy usage for In-App Automations, Scenes, and Surveys that was introduced in 18.0.0. This issue is not very common but it can occur if the device is unable to connect to our backend to fetch an update to the In-App rules on the device after an SDK update or locale change. Application that are receiving high energy usage reports should update. ### Changes - Fixed high energy usage for In-App Automations, Scenes, and Surveys if remote-data fails to refresh. - Fixed requesting additional notification options if they change after the first prompt. ## 18.9.1 September 13, 2024 Patch release to fix Scene button not able to be tapped in some cases. #### Changes - Fix Scene buttons not able to be tapped if the last page of the scene contains a wide image background. ## 18.9.0 September 10, 2024 Minor release that introduces `fallback` parameter when requesting permission updates and the permission is denied. This release also contains a fix for a regression in 18.8.0 where Channel Registration would continuously update for channels that have upgraded from an earlier SDK versions. Applications using 18.8.0 should update. #### Changes - Added new method `Airship.permissionsManager.requestPermission(_:enableAirshipUsageOnGrant:fallback:)` and `Airship.push.enableUserPushNotifications(fallback:)` that allows you to specify a fallback behavior if the permission is already denied. - Fixed high CPU issues with embedded messages that define a percent based size. - Fixed Channel Registration bug that was introduced in 18.8. ## 18.8.0 September 6, 2024 Minor release with several enhancements to In-App Automation, Scenes, and Surveys. ### Changes - Added support to disable plain markdown (text markup) support in a Scene. - Added support to theme markdown links in a Scene. - Added execution window support to In-App Automation, Scenes, and Surveys. - Added `displayNotificationStatus` status to the `AirshipNotificationStatus` object to get the user notification permission status. - Added `Airship.permissionManager.statusUpdates(for:)` that returns an async stream of permission status updates. - Added `MessageCenter.shared.inbox.unreadCountUpdates` that returns an async stream of unread count updates. - Added `MessageCenter.shared.inbox.messageUpdates` that returns an async stream of message updates. - Updated handling of priority for In-App Automation, Scenes, and Surveys. Priority is now taken into consideration at each step of displaying a message instead of just sorting messages that are triggered at the same time. - Updated handling of long delays for In-App Automation, Scenes, and Surveys. Delays will now be preprocessed up to 30 seconds before it ends before the message is prepared. - Fixed Message Center theme loader when trying to theme the OOTB Message Center window. ## 18.7.2 August 10, 2024 Patch release that fixes in-app experience displays when resuming from a paused state. Apps that use in-app experiences are encouraged to update. ### Changes - Fixed Automation Engine updates when pause state changes. ## 18.7.1 August 1, 2024 Patch release that prevents In-App Automation, Scenes, and Surveys from being able to trigger off custom events or screen views when analytics is disabled. The actual event was not being tracked by Airship in these cases, just processed locally. ### Changes - Prevent screen view and custom events from being processed by automations when analytics is disabled. ## 18.7.0 July 30, 2024 Minor release that fixes some layout issues with images and videos in a Scene, accessibility improvements, and fixes a potential crash with JSON encoding/decoding due to using a JSONEncoder/JSONDecoder across threads. ### Changes - Fixed video & image scaling/cropping in scenes. - Removed reusing `JSONEncoder`/`JSONDecoder` across tasks. - Removed `@MainActor` requirement from `AirshipPush.authorizedNotificationSettings`. - Announce screen changes when banners In-App messages are displayed. - `MessageCenterController` is now optional when creating a `MessageCenterView`. ## 18.6.0 July 16, 2024 Minor release with some improvements to preference center, a fix for in-app message veritcal sizing, accessibility improvements and markdown support in scenes. ### Changes - Added warning message to preference center email entry field. - Updated preference center country_code. - Fixed bug preventing preference center channel management from fully opting-out registered channels. - Fixed padding bug preventing modal in-app messages from properly sizing to their content. - Added accessibility improvements. - Added markdown support to scenes. ## 18.5.0 July 1, 2024 Minor release that includes cert pinning and various fixes and improvements for Preference Center, In-app Messages and Embedded Content. ### Changes - Added ability to inject a custom certificate verification closure that applies to all API calls - Added width and height parameters to in-app dismiss button theming - Fixed bug that caused HTML in-app message backgrounds to default to clear instead of system background - Fixed extra payload parsing in in-app messages - Set default banner placement to bottom - Increased impression interval for embedded in-app views - Improved in-app banner view accessibility - Preference center contact channel listing is now refreshed on foreground and from background pushes ## 18.4.1 June 21, 2024 Patch release to fix a regression with In-App Automations, Scenes, and Surveys ignoring screen, version, and custom event triggers. Apps using those triggers that are on 18.4.0 should update. ### Changes - Fixed trigger regression for IAX introduced in 18.4.0. ## 18.4.0 June 14, 2024 Minor release that adds contact management support to the preference center, support for anonymous channels, per-message in-app message theming, message center customization and logging improvements. Apps that use the message center or stories should update to this version. ### Changes - Added support for anonymous channels - Added contact management support in preference centers - Added improved theme support and per message theming for in-app messages - Added public logging functions - Fixed bug in stories page indicator - Fixed message center list view background theming ## 18.3.1 May 27, 2024 Patch release with bug fix for message center customization. Apps that use the message center should update to this version. ### Changes - Fixed background color application in message center. ## 18.3.0 May 21, 2024 Minor release with updates to message center customization, a bug fix for story pager transition animation and a bug fix for in-app banner button rendering. ### Changes - Fixed in-app message banner button rendering. - Fixed story pager transition animation. - Added message center list and list container background color customization via new plist keys `messageListBackgroundColor`, `messageListBackgroundColorDark`, `messageListContainerBackgroundColor` and `messageListContainerBackgroundColorDark` ## 18.2.2 May 16, 2024 Patch release includes a fix for submission issues when building with XCFrameworks, a bug fix for emitting pager events from in-app pager views, and a bug fix for the in-app banner's default title and body alignment to match the dashboard preview. Apps using XCFrameworks should update. ### Changes - Fixed pager event emission from in-app pager views. - Fixed submission issue when building with XCFrameworks. - Fixed in-app banner title and body default alignment. [View Older Releases](https://github.com/urbanairship/ios-library/releases?q=created%3A%3C2024-05-15&expanded=true) # Apple SDK Resources > SDK modules, API references, and other resources for iOS, tvOS, and visionOS development. ## Platform Support {#platform-support} | Feature | iOS | tvOS | visionOS | |----------------------------------------|-----|-------|----------| | Push Notifications | ✅ | ✅ | ✅ | | Live Activities | ✅ | ❌ | ❌ | | In-App Experiences | ✅ | ✅ ¹ | ✅ | | Embedded Content | ✅ | ✅ | ✅ | | Message Center | ✅ | ❌ | ✅ | | Preference Center | ✅ | ✅ | ✅ | | Feature Flags | ✅ | ✅ | ✅ | | Analytics | ✅ | ✅ | ✅ | | Contacts | ✅ | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ✅ | | SwiftUI Support | ✅ | ✅ | ✅ | ¹ **tvOS In-App Experiences:** Scenes, Banners, and non-HTML In-App Automations are supported. However, scheduled In-App Experiences will no longer display if the app's cache is wiped due to tvOS storage limitations. s ## API references - [AirshipCore](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore) - [AirshipMessageCenter](https://urbanairship.github.io/ios-library/v20/AirshipMessageCenter/documentation/airshipmessagecenter/) - [AirshipAutomation](https://urbanairship.github.io/ios-library/v20/AirshipAutomation/documentation/airshipautomation/) - [AirshipPreferenceCenter](https://urbanairship.github.io/ios-library/v20/AirshipPreferenceCenter/documentation/airshippreferencecenter/) - [AirshipFeatureFlags](https://urbanairship.github.io/ios-library/v20/AirshipFeatureFlags/documentation/airshipfeatureflags/) - [AirshipObjectiveC](https://urbanairship.github.io/ios-library/v20/AirshipObjectiveC/documentation/airshipobjectivec/) - [AirshipNotificationServiceExtensions](https://urbanairship.github.io/ios-library/v20/AirshipNotificationServiceExtension/documentation/airshipnotificationserviceextension/) ## Github Samples * [Sample Apps](https://github.com/urbanairship/apple-sample-apps) — includes tvOS ## Source * [Source](https://github.com/urbanairship/ios-library) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [iOS license](https://github.com/urbanairship/ios-library/blob/main/LICENSE). ### SDK Installation Complete installation and configuration guides for the Airship SDK, including setup, advanced integration, logging, and locale configuration. # Install and Set Up the Apple SDK > Learn how to install the Airship SDK using SPM, CocoaPods, Carthage, or xcframeworks, and initialize the SDK in your iOS, tvOS, or visionOS applications. The Airship SDK is a modern, Swift 6-native SDK designed for Apple platforms. It provides type-safe, actor-isolated APIs with full Swift concurrency support that work seamlessly across iOS, tvOS, and visionOS — all from a single SDK. For a complete reference of feature support across iOS, tvOS, and visionOS, see [Platform Support](https://www.airship.com/docs/developer/sdk-integration/apple/resources/#platform-support). > **Tip:** If you use an AI coding assistant, you can connect it to Airship with Skills and an MCP server. See [Airship AI Tools](https://www.airship.com/docs/developer/ai-tools/ai-tools/). ## Requirements * Minimum iOS version: 16.0+ * Minimum tvOS version: 18.0+ * Minimum visionOS version: 1.0+ * Requires Xcode 26.0+ ## SDK installation The Airship SDK can be installed using SPM (Swift Package Manager), CocoaPods, Carthage, or xcframeworks. We recommend SPM for new projects. #### SPM 1. In your Xcode project, select your project in the Project Navigator. 2. Select your target, then go to the **Package Dependencies** tab. 3. Click the **+** button to add a package. 4. Enter the package URL: `https://github.com/urbanairship/ios-library` 5. Select the version rule (recommended: "Up to Next Major Version"). 6. Click **Add Package**. 7. Select the Airship package products you want to include in your app: **Available package products:** - `AirshipBasement` : Required by AirshipCore - `AirshipCore` : Push messaging features including channels, tags, named user and default actions (required) - `AirshipMessageCenter` : Message center - `AirshipAutomation` : Automation and in-app messaging - `AirshipPreferenceCenter` : Preference Center - `AirshipFeatureFlags` : Feature Flags - `AirshipObjectiveC` : Objective-C Bindings - `AirshipDebug` : Debugging tools - `AirshipNotificationServiceExtension` : Service Extension framework (only for Notification Service Extension targets, not the app target)* 8. Click **Add Package**. 9. Import the modules in your code where needed. Import statements match the module names: ```swift import AirshipCore import AirshipMessageCenter import AirshipAutomation ``` For more details, see Apple's guide on [adding package dependencies to your app](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app). #### Cocoapods > **Note:** CocoaPods trunk is [moving to read-only mode in December 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/). Airship will continue to support CocoaPods as long as possible, but we recommend using SPM (Swift Package Manager) for new projects. Existing CocoaPods installations will continue to work after trunk becomes read-only. 1. Install CocoaPods if you haven't already: `$ gem install cocoapods` 2. Navigate to your project directory in Terminal. 3. Create a `Podfile` (if one doesn't exist): `$ pod init` 4. Open your `Podfile` and add the Airship pod. The `Airship` pod is modular and divided into subspecs: **Available subspecs:** - `Airship/Core` : Push messaging features including channels, tags, named user and default actions (required) - `Airship/MessageCenter` : Message center - `Airship/Automation` : Automation and in-app messaging - `Airship/PreferenceCenter` : Preference Center module - `Airship/FeatureFlags` : Feature Flags module - `Airship/ObjectiveC` : Objective-C bindings ```ruby target "" do pod 'Airship' end ``` **Or specify individual subspecs:** ```ruby target "" do pod 'Airship/Core' pod 'Airship/MessageCenter' pod 'Airship/Automation' pod 'Airship/FeatureFlags' end ``` **For tvOS projects, specify the platform:** ```ruby platform :tvos, '18.0' target "" do pod 'Airship' end ``` 5. Install the pods: `$ pod install` 6. **Important:** After running `pod install`, an Xcode workspace (`.xcworkspace`) file is generated. Always open the workspace file instead of the project file (`.xcodeproj`) when building your project. If you encounter issues, see the [CocoaPods troubleshooting guide](http://guides.cocoapods.org/using/troubleshooting.html). #### Carthage 1. Install Carthage if you haven't already. See the [Carthage installation guide](https://github.com/Carthage/Carthage#installing-carthage). 2. Verify `Enable Modules` and `Link Frameworks Automatically` are enabled in your project's Build Settings. 3. Follow Carthage's [adding frameworks to an application](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) instructions to add frameworks to your application. 4. Specify the Airship iOS SDK in your `Cartfile`: ```text github "urbanairship/ios-library" ``` 5. Build the frameworks: `$ carthage update` 6. Add the frameworks to your project. Airship is modular, so select only the frameworks you need: **Available frameworks:** - `AirshipBasement` : Required by AirshipCore - `AirshipCore` : Push messaging features including channels, tags, named user and default actions (required) - `AirshipMessageCenter` : Message center - `AirshipAutomation` : Automation and in-app messaging - `AirshipPreferenceCenter` : Preference Center - `AirshipFeatureFlags` : Feature Flags - `AirshipObjectiveC` : Objective-C Bindings - `AirshipDebug` : Debugging tools - `AirshipNotificationServiceExtension` : Service Extension framework (only for Notification Service Extensions)* 7. Import the frameworks in your code. Import statements match the framework names: ```swift import AirshipCore import AirshipMessageCenter import AirshipAutomation ``` #### xcframeworks 1. Download and decompress the latest version of the [iOS SDK](https://github.com/urbanairship/ios-library/releases). 2. Inside the folder you should see a collection of XCFrameworks. Airship is modular, so select only the XCFrameworks you need: **Available XCFrameworks:** - `AirshipBasement.xcframework` : Required by AirshipCore - `AirshipCore.xcframework` : Push messaging features including channels, tags, named user and default actions (required) - `AirshipMessageCenter.xcframework` : Message center - `AirshipAutomation.xcframework` : Automation and in-app messaging - `AirshipPreferenceCenter.xcframework` : Preference Center - `AirshipFeatureFlags.xcframework` : Feature Flags - `AirshipObjectiveC.xcframework` : Objective-C Bindings - `AirshipDebug.xcframework` : Debugging tools - `AirshipNotificationServiceExtension.xcframework` : Service Extension framework (only for Notification Service Extensions)* 3. Add XCFrameworks to your project: - Open your project in Xcode - Click on your project in the Project Navigator - Select your target - Make sure the General tab is selected - Scroll down to **Frameworks, Libraries, and Embedded Content** - Drag in desired XCFrameworks from the downloaded SDK. They are wired up automatically as dependencies of your target 4. Verify Build Settings: - `Enable Modules` should be set to `Yes` - `Link Frameworks Automatically` should be set to `Yes` ![Enable Modules build setting in Xcode](https://www.airship.com/docs/images/enable-modules_hu_f56ed603b2699427.webp) *Enable Modules build setting in Xcode* ![Link Frameworks Automatically build setting in Xcode](https://www.airship.com/docs/images/link-frameworks-automatically_hu_e08f4e14a1a2ca09.webp) *Link Frameworks Automatically build setting in Xcode* 5. Import the frameworks in your code. Import statements match the framework names: ```swift import AirshipCore import AirshipMessageCenter import AirshipAutomation ``` ## Initialize Airship The Airship SDK requires only a single entry point, known as *takeOff*. For UIKit apps, initialize during the application delegate's `application(_:didFinishLaunchingWithOptions:)` method. For SwiftUI apps, you can initialize in the App's `init()` method. Before calling `takeOff`, configure the following: - **Project Credentials**: Airship requires your project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret)to authenticate your application. To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. You need separate credentials for development and production environments. On iOS, this is necessary because Apple provides separate APNS (Apple Push Notification Service) environments: - **Development/Sandbox**: Used for testing and development builds - **Production**: Used for App Store and TestFlight builds The SDK automatically selects the correct credentials based on your build configuration. Configure both sets of credentials in your code, and use the `#if DEBUG` conditional to switch between environments. - **Cloud Site**: Airship config defaults to the US cloud site. If your application is set up for the EU site, set the site on the config options to `.eu`. ### Calling takeOff The following examples show how to configure and call `takeOff` programmatically. Alternatively, you can configure Airship using an `AirshipConfig.plist` file—see [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/apple/installation/advanced-integration/#configuring-airship-via-plist-file) for details. #### Swift ```swift import SwiftUI import AirshipCore @main struct MyApp: App { init() { var config = AirshipConfig() // Set credentials config.productionAppKey = "YOUR PRODUCTION APP KEY" config.productionAppSecret = "YOUR PRODUCTION APP SECRET" config.developmentAppKey = "YOUR DEVELOPMENT APP KEY" config.developmentAppSecret = "YOUR DEVELOPMENT APP SECRET" // Set cloud site (.us or .eu) config.site = .us #if DEBUG config.inProduction = false config.isAirshipDebugEnabled = true #else config.inProduction = true #endif try! Airship.takeOff(config) } var body: some Scene { WindowGroup { ContentView() } } } ``` For **UIKit** apps, call `takeOff` in your `AppDelegate`'s `application(_:didFinishLaunchingWithOptions:)` method instead of the App's `init()`. #### Objective-C ```objective-c @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { UAConfig *config = [UAConfig config]; // Set credentials config.productionAppKey = @"YOUR PRODUCTION APP KEY"; config.productionAppSecret = @"YOUR PRODUCTION APP SECRET"; config.developmentAppKey = @"YOUR DEVELOPMENT APP KEY"; config.developmentAppSecret = @"YOUR DEVELOPMENT APP SECRET"; // Set cloud site (UACloudSiteUS or UACloudSiteEU) config.site = UACloudSiteUS; #if DEBUG config.inProduction = NO; config.isAirshipDebugEnabled = YES; #else config.inProduction = YES; #endif NSError *airshipError; [UAirship takeOff:config error:&airshipError]; NSAssert(airshipError == nil, @"TakeOff failed %@", airshipError); return YES; } @end ``` The Airship SDK automatically integrates with your app by default, so you don't need to implement push-related `UIApplicationDelegate` or `UNUserNotificationCenterDelegate` methods. This works for most applications out of the box. For advanced use cases or to disable automatic integration, see the [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/apple/installation/advanced-integration/##manual-integration) guide. ## Test the integration After completing the setup, verify your integration: 1. **Build and run your app** in Xcode 2. **Check the console logs** for Airship channel creation: - Look for a log message: `Channel ID: ` - The channel ID will be displayed in the console output - For more detailed logging, see [Logging](https://www.airship.com/docs/developer/sdk-integration/apple/installation/logging/) If you see the channel ID in the console logs and no errors, your integration is successful. You can now proceed with configuring [deep links](https://www.airship.com/docs/developer/sdk-integration/apple/deep-links/), [push notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/getting-started/), and other Airship features. If you don't see a channel ID in the console logs or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/apple/troubleshooting/initialization/) for common problems and solutions. # Advanced Integration > Disable automatic integration, manually forward app delegate methods, and configure URL allowlists for the Airship SDK. ## Configuring Airship via plist file If no config is provided to `takeOff` programmatically, Airship will default to loading config from the `AirshipConfig.plist` file in your application's bundle. This can be useful for apps with multiple build variants, or for keeping credentials out of version control. Sample `AirshipConfig.plist` file: ```xml detectProvisioningMode developmentAppKey Your Development App Key developmentAppSecret Your Development App Secret productionAppKey Your Production App Key productionAppSecret Your Production App Secret ``` The keys used in the `AirshipConfig.plist` file match the field names in [AirshipConfig](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/airshipconfig) . If your app uses Airship's EU cloud site, add the `site` key: ```xml site EU ``` If you don't see a Channel ID in the console logs or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/apple/troubleshooting/initialization/). ## Manual Integration By default, the Airship SDK automatically integrates with your app using *method swizzling*. This allows the SDK to intercept app delegate messages and forward them automatically, so you don't need to implement push-related `UIApplicationDelegate` or `UNUserNotificationCenterDelegate` protocol methods. For most applications, automatic integration works out of the box. However, if you have custom app delegate requirements or prefer explicit control over method forwarding, you can disable automatic integration and handle it manually. ### Disabling automatic integration Set [AirshipConfig.isAutomaticSetupEnabled](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/airshipconfig/isautomaticsetupenabled) to `false` in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff): #### Swift ```swift var config = AirshipConfig() config.isAutomaticSetupEnabled = false try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; config.isAutomaticSetupEnabled = NO; [UAirship takeOff:config error:&airshipError]; ``` ### Forwarding app delegate methods When automatic integration is disabled, you must forward the appropriate app delegate methods to the Airship SDK. #### Swift `UIApplicationDelegate` methods: ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Set up the UNUserNotificationCenter delegate UNUserNotificationCenter.current().delegate = self return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { AppIntegration.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { AppIntegration.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { return await AppIntegration.application(application, didReceiveRemoteNotification: userInfo) } ``` `UNUserNotificationCenterDelegate` methods: ```swift func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping @Sendable () -> Void ) { // Call through to Airship. The completion handler version is called on the main actor. // It's important to use the `completionHandler` version and not the async one, or it // will be called on a background thread and Airship might not receive the event in time // to count the direct open. MainActor.assumeIsolated { AppIntegration.userNotificationCenter( center, didReceive: response, withCompletionHandler: completionHandler ) } } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { return await AppIntegration.userNotificationCenter(center, willPresent: notification) } ``` #### Objective-C `UIApplicationDelegate` methods: ```objc - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Set up the UNUserNotificationCenter delegate [UNUserNotificationCenter currentNotificationCenter].delegate = self; return YES; } - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [UAAppIntegration application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [UAAppIntegration application:application didFailToRegisterForRemoteNotificationsWithError:error]; } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { [UAAppIntegration application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; } ``` `UNUserNotificationCenterDelegate` methods: ```objc - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { [UAAppIntegration userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler]; } - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { [UAAppIntegration userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; } ``` ## URL allowlist The [URLAllowList](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/airshipurlallowlist) controls which URLs the Airship SDK is able to act on. The SDK divides up usages of URLs into two different scopes: - `SCOPE_OPEN_URL`: Only URLs allowed for this scope can be opened from an action, displayed in landing page, or displayed in an HTML in-app message. Defaults to allowing all URLs if not specified in the config. - `SCOPE_JAVASCRIPT_INTERFACE`: These URLs are checked before the Airship JavaScript interface is injected into the webview. Defaults to any Airship originated URLs. Allowed URLs should be provided when configuring the Airship Config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff). #### Swift ```swift var config = AirshipConfig() // Allow all URLs for both scopes config.urlAllowList = ["*"] // Or configure specific scopes: config.urlAllowListScopeOpenURL = ["https://example.com/*", "https://*.youtube.com/*"] config.urlAllowListScopeJavaScriptInterface = ["https://example.com/*"] try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; // Allow all URLs for both scopes config.urlAllowList = @[@"*"]; // Or configure specific scopes: config.urlAllowListScopeOpenURL = @[@"https://example.com/*", @"https://*.youtube.com/*"]; config.urlAllowListScopeJavaScriptInterface = @[@"https://example.com/*"]; [UAirship takeOff:config error:&airshipError]; ``` **Valid URL pattern syntax** ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` # Logging > Configure log levels, privacy settings, and custom log handlers to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. If you don't configure logging, the SDK uses **Info** for development builds and **Error** for production builds with **private** privacy level. ## Log levels The log level acts as a minimum threshold—only logs at that level and higher will be logged. Available log levels, ordered from most to least verbose: | Log Level | Prefix | Description | | :-------- | :----- | :---------- | | **Verbose** | `[Airship] [V]` | Highly detailed SDK status for deep debugging and troubleshooting | | **Debug** | `[Airship] [D]` | General SDK status with more detailed information than Info | | **Info** | `[Airship] [I]` | General SDK status and lifecycle events | | **Warning** | `[Airship] [W]` | API deprecations, invalid setup, and other recoverable issues | | **Error** | `[Airship] [E]` | Critical errors and exceptions that the SDK cannot gracefully handle | | **None** | — | Disables all logging | ## Log privacy levels Control the visibility of log contents using privacy levels. This is especially useful when debugging release builds without exposing sensitive information. - **private** (default): Uses `os.Logger` to log all messages at the `private` level. The content of most logs will be redacted and will not be visible in the Console app by default. Use this for production builds to protect sensitive data. - **public**: Sends all logs to `os.Logger` with a `public` privacy level, preventing their content from being redacted. Use this when you need to capture detailed logs from release builds for debugging. > **Note:** When using `public` privacy level, `verbose` and `debug` log messages are automatically elevated to `info` level because Console log doesn't support those levels directly. This ensures all detailed logs are visible when debugging production builds. ## Configuration You can set separate log levels and privacy levels for development and production builds in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff). ### Common configuration Typical setup: more verbose logging for development, minimal logging for production: #### Swift ```swift var config = AirshipConfig() // Development: verbose logging for debugging config.developmentLogLevel = .verbose config.developmentLogPrivacyLevel = .public // Production: minimal logging to reduce noise config.productionLogLevel = .error config.productionLogPrivacyLevel = .private try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; // Development: verbose logging for debugging config.developmentLogLevel = UAAirshipLogLevelVerbose; // Production: minimal logging to reduce noise config.productionLogLevel = UAAirshipLogLevelError; // Privacy levels (set via AirshipConfig.plist) // developmentLogPrivacyLevel // public // productionLogPrivacyLevel // private [UAirship takeOff:config error:&airshipError]; ``` ### Debugging production issues When debugging issues in production builds, temporarily enable verbose logging to capture detailed SDK behavior: #### Swift ```swift var config = AirshipConfig() // Production debugging: enable verbose logs config.productionLogLevel = .verbose config.productionLogPrivacyLevel = .public try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; // Production debugging: enable verbose logs config.productionLogLevel = UAAirshipLogLevelVerbose; // Set via AirshipConfig.plist: // productionLogPrivacyLevel // public [UAirship takeOff:config error:&airshipError]; ``` ## Custom log handler You can provide a custom log handler to intercept and handle all Airship log messages. This is useful when you need to integrate Airship logs with your own logging system or customize how logs are formatted or stored. When a custom log handler is set, the default Airship log handler is completely replaced. Log level filtering is performed before your handler is called, so your handler will only receive logs that meet the configured log level threshold. Implement the `AirshipLogHandler` protocol and set it on your Airship config: ```swift import os.log final class CustomLogHandler: AirshipLogHandler { private let logger = Logger(subsystem: "com.yourapp.airship", category: "Airship") func log( logLevel: AirshipLogLevel, message: String, fileID: String, line: UInt, function: String ) { // Forward to your logging system let osLogLevel: OSLogType switch logLevel { case .verbose, .debug: osLogLevel = .debug case .info: osLogLevel = .info case .warning: osLogLevel = .default case .error: osLogLevel = .error case .none: return } logger.log(level: osLogLevel, "\(message)") // Optionally: send to remote logging service // YourLoggingService.log(message, level: logLevel) } } var config = AirshipConfig() config.logHandler = CustomLogHandler() try! Airship.takeOff(config) ``` # Locale > Configure locale behavior and override the default locale that Airship uses. Airship uses the [Locale](https://www.airship.com/docs/reference/glossary/#locale) for various SDK operations. By default, the SDK automatically uses the device's locale settings, but you can configure it to use the user's preferred language or override it programmatically. ## Configuring locale behavior You can configure how Airship determines the locale by setting the `useUserPreferredLocale` option in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff). By default, `useUserPreferredLocale` is `false`, and the SDK uses `Locale.autoupdatingCurrent`, which reflects the device's current locale settings. When set to `true`, the SDK uses the first language from the user's preferred languages list (`Locale.preferredLanguages[0]`), which is useful when you want the SDK to match the user's language preference rather than the device's current locale settings. #### Swift ```swift var config = AirshipConfig() // ... other config settings ... // Use preferred language instead of current locale config.useUserPreferredLocale = true try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; // ... other config settings ... // Use preferred language instead of current locale config.useUserPreferredLocale = YES; [UAirship takeOff:config error:&airshipError]; ``` ## Overriding the locale You can override the locale programmatically at runtime, which takes precedence over both the configured locale behavior and the device's locale settings. #### Swift ```swift Airship.localeManager.currentLocale = Locale(identifier:"de") ``` #### Objective-C ```objc UAirship.localeManager.currentLocale = [NSLocale localeWithLocaleIdentifier:@"de"]; ``` ## Clearing the locale override To remove a locale override and return to using the configured locale behavior: #### Swift ```swift Airship.localeManager.clearLocale() ``` #### Objective-C ```objc [UAirship.localeManager clearLocale]; ``` ## Getting the current locale To retrieve the locale that Airship is currently using: #### Swift ```swift let airshipLocale = Airship.localeManager.currentLocale ``` #### Objective-C ```objc NSLocale *airshipLocale = UAirship.localeManager.currentLocale; ``` ### Push Notifications Comprehensive guides for implementing push notifications, including setup, rich media support, interactive notifications, badge management, quiet time, and more. # Push Notifications > How to configure your application to receive and respond to notifications. Before setting up push notifications in your app, you need to configure APNs (Apple Push Notification service) in the Airship dashboard. See [iOS Channel Configuration](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration) for instructions on uploading your APNs certificate or token. ## Enable Capabilities Before enabling push notifications, you need to configure your app's capabilities in Xcode. ### Enable Push Notifications Capability 1. Open your project in Xcode. 2. Click on your project in the Project Navigator. 3. Select your main app target and then click the **Signing & Capabilities** tab. 4. If you do not see Push Notifications enabled, click **+ Capability** and add **Push Notifications**. ![Adding the Push Notifications capability in Xcode](https://www.airship.com/docs/images/ios-enable-push-notifications-capabilities_hu_2e1789fffb02612b.webp) *Adding the Push Notifications capability in Xcode* ### Enable Background Modes 1. Select your main app target and then click the **Signing & Capabilities** tab. 2. Click **+ Capability** and add **Background Modes**. ![Adding the Background Modes capability in Xcode](https://www.airship.com/docs/images/ios-enable-background-mode-capabilities_hu_f135d9fec0ba0d06.webp) *Adding the Background Modes capability in Xcode* 3. In the **Background Modes** section, select the **Remote notifications** checkbox. ![Enabling Remote notifications in Background Modes](https://www.airship.com/docs/images/ios-background-mode-remote-notifications_hu_7e38b08288fcd7b2.webp) *Enabling Remote notifications in Background Modes* ## Add Rich Media Support To support rich media attachments (images, animated GIFs, video) in push notifications, you need to create a Notification Service Extension. See [Notification Service Extension](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/) for setup instructions. ## Enable User Notifications The Airship SDK distinguishes between *user notifications* (visible to users) and *silent push notifications* (background data delivery). User notifications require explicit permission from the user. By default, user notifications are disabled. Enable them when you want to show visible notifications to users. ### Basic Enablement The simplest way to enable user notifications is to set the `userPushNotificationsEnabled` property: #### Swift ```swift Airship.push.userPushNotificationsEnabled = true ``` #### Objective-C ```objc UAirship.push.userPushNotificationsEnabled = YES; ``` ### Checking Authorization State To check whether the user granted permission, use the async method which returns the system authorization state: #### Swift ```swift let authorized = await Airship.push.enableUserPushNotifications() if authorized { // User granted permission } else { // User denied permission } ``` #### Objective-C > **Note:** This async method is not available in Objective-C. Use the basic `userPushNotificationsEnabled` property instead. > **Note:** The return value represents the **system authorization state** (whether the user granted permission), not the state of `userPushNotificationsEnabled`, which will always be set to `true` after calling this method. ### Handling Denied Permissions If the user has already denied notification permissions, you can provide a fallback action to guide them to system settings or show a custom message: #### Swift ```swift // Navigate to system settings if permission is denied let authorized = await Airship.push.enableUserPushNotifications( fallback: .systemSettings ) // Or provide a custom callback let authorized = await Airship.push.enableUserPushNotifications( fallback: .callback { // Show custom UI explaining why notifications are important // and guide user to system settings } ) // Or no fallback let authorized = await Airship.push.enableUserPushNotifications( fallback: .none ) ``` #### Objective-C > **Note:** This async method is not available in Objective-C. Use the basic `userPushNotificationsEnabled` property instead. The `PromptPermissionFallback` options are: - `.none` - No fallback action - `.systemSettings` - Automatically navigate to system settings if permission is denied - `.callback` - Execute a custom callback to handle the denied state > **Tip:** To increase the likelihood that users will accept notification permissions, avoid prompting immediately on app launch. Instead, wait for a more appropriate moment, such as after the user completes an action or views relevant content. ## Configure Notification Options Before enabling user notifications, you can configure which notification types your app will request permission for. ### Standard Notification Options By default, the Airship SDK requests permission for alerts, badges, and sounds. You can customize these options: #### Swift ```swift Airship.push.notificationOptions = [.alert, .badge, .sound] ``` #### Objective-C ```objc UAirship.push.notificationOptions = (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert); ``` ### Provisional Authorization Provisional authorization allows you to send notifications without initially prompting the user. Notifications are delivered quietly to the Notification Center until the user explicitly chooses to keep them. #### Swift ```swift Airship.push.notificationOptions = [.alert, .badge, .sound, .provisional] ``` #### Objective-C ```objc UAirship.push.notificationOptions = (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionProvisional); ``` > **Note:** With provisional authorization, you can programmatically enable user notifications without showing a permission prompt. This is still required for any visible notification delivery. ## Foreground Presentation Options When your app is in the foreground, iOS silences notifications by default. Configure how notifications are displayed when the app is active: #### Swift ```swift Airship.push.defaultPresentationOptions = [.alert, .badge, .sound] ``` #### Objective-C ```objc UAirship.push.defaultPresentationOptions = (UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound); ``` ## Handle Notification Events The Airship SDK provides callbacks for when notifications are received or interacted with. These callbacks are optional—the SDK will handle notifications automatically if you don't set them. #### Swift ```swift // Handle when user taps a notification Airship.push.onReceivedNotificationResponse = { response in // Handle notification response } // Handle notification received while app is in foreground Airship.push.onReceivedForegroundNotification = { userInfo in // Handle foreground notification } // Handle background content-available notification Airship.push.onReceivedBackgroundNotification = { userInfo in // Handle background notification return .noData } // Customize presentation options per notification Airship.push.onExtendPresentationOptions = { options, notification in // Return presentation options for this specific notification return [.badge, .list, .banner, .sound] } ``` #### Objective-C ```objc // Handle when user taps a notification UAirship.push.onReceivedNotificationResponse = ^(UNNotificationResponse *response) { // Handle notification response }; // Handle notification received while app is in foreground UAirship.push.onReceivedForegroundNotification = ^(NSDictionary *userInfo) { // Handle foreground notification }; // Handle background content-available notification UAirship.push.onReceivedBackgroundNotification = ^UIBackgroundFetchResult(NSDictionary *userInfo) { // Handle background notification return UIBackgroundFetchResultNoData; }; // Customize presentation options per notification UAirship.push.onExtendPresentationOptions = ^UNNotificationPresentationOptions(UNNotificationPresentationOptions options, UNNotification *notification) { // Return presentation options for this specific notification return UNNotificationPresentationOptionList | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; }; ``` ## Silent Notifications Silent notifications are push messages that don't display a notification to the user. They're typically used to wake your app in the background to perform tasks or fetch content. To send a silent notification, set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). > **Important:** Thoroughly test your implementation to confirm that silent notifications don't generate any visible device notifications. > **Note:** Silent notifications (`content_available`) don't have guaranteed delivery. Factors affecting delivery include battery life, WiFi connectivity, and the number of silent pushes sent recently. These metrics are determined solely by iOS and APNs. > > Use silent notifications to supplement your app's regular behavior rather than for critical functionality. For example, use them to pre-fetch data ahead of time to reduce load times when the user launches the app. ## Next Steps - [Notification Service Extension](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/) - Add rich media support to push notifications - [Interactive Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/interactive-notifications/) - Add action buttons to notifications - [Badge Management](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/badge-management/) - Set, reset, or enable auto-badge functionality - [Quiet Time](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/quiet-time/) - Suppress notifications during specific hours - [App Clips](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/app-clips/) - Configure push notifications for App Clips If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/troubleshooting/push-notifications/) to check notification status and fix common issues. # Notification Service Extension > Create and configure a notification service extension to support rich media attachments like images, animated GIFs, and videos in push notifications. To support rich media attachments (images, animated GIFs, video) in push notifications, you need to create a [notification service extension](https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications) (NSE). ## Create a Notification Service Extension Target 1. In Xcode, click **File** → **New** → **Target...**. 2. Select **Notification Service Extension**. 3. Click **Next** and configure your extension: - **Product Name**: Your extension name (e.g., `NotificationServiceExtension`) - **Bundle Identifier**: Typically your app's bundle ID with a suffix (e.g., `com.example.app.NotificationServiceExtension`) ![Creating a Notification Service Extension target in Xcode](https://www.airship.com/docs/images/create-notification-service-extension_hu_8bb42b1a35cc5e03.webp) *Creating a Notification Service Extension target in Xcode* 4. Verify that your app target's **Embed App Extensions** includes the newly created extension. ![Verifying the extension is embedded in the app target](https://www.airship.com/docs/images/embed-extension-in-app_hu_393854a222d22be6.webp) *Verifying the extension is embedded in the app target* ## Install Dependencies #### SPM 1. Select your service extension target in the Project Navigator. 2. Go to the **Package Dependencies** tab. 3. If you haven't already added the Airship package, click **+** and add: `https://github.com/urbanairship/ios-library` 4. Select the `AirshipNotificationServiceExtension` package product for your service extension target. > **Note:** The `AirshipNotificationServiceExtension` package should only be added to the Notification Service Extension target, not the main app target. 5. Import the module: ```swift import AirshipNotificationServiceExtension ``` #### CocoaPods Add to your `Podfile`: ```ruby target "" do pod 'AirshipServiceExtension' end ``` Install: `$ pod install` #### Carthage 1. Add `AirshipNotificationServiceExtension.framework` to your service extension target following [Carthage's instructions](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). 2. Add to your `Cartfile`: ```text github "urbanairship/ios-library" ``` 3. Build: `$ carthage update` 4. Verify that **Enable Modules** and **Link Frameworks Automatically** are enabled in Build Settings. #### xcframeworks 1. Download the latest [iOS SDK release](https://github.com/urbanairship/ios-library/releases). 2. Add `AirshipNotificationServiceExtension.xcframework` to your **main app target**: - Select your main app target - Go to **General** → **Frameworks, Libraries, and Embedded Content** - Drag in `AirshipNotificationServiceExtension.xcframework` - Set **Embed** to **Embed & Sign** 3. Add `AirshipNotificationServiceExtension.xcframework` to your **service extension target**: - Select your service extension target - Go to **General** → **Frameworks, Libraries, and Embedded Content** - Add `AirshipNotificationServiceExtension.xcframework` - Set **Embed** to **Do Not Embed** 4. Configure the service extension's runpath: - Select your service extension target - Go to **Build Settings** → search for **Runpath Search Paths** (`LD_RUNPATH_SEARCH_PATHS`) - Add: `@executable_path/../../Frameworks` 5. Verify Build Settings for both targets: - **Enable Modules**: `Yes` - **Link Frameworks Automatically**: `Yes` > **Note:** The framework must be embedded in the main app target, not the extension. Extensions cannot contain nested frameworks, which will cause App Store rejection. The runpath setting allows the extension to find the framework in the main app's `Frameworks` directory. ## Implement the Service Extension Replace the default `NotificationService` implementation with Airship's base class: #### Swift (SPM/Carthage/xcframeworks) ```swift import AirshipNotificationServiceExtension class NotificationService: UANotificationServiceExtension { } ``` #### Swift (CocoaPods) ```swift import AirshipServiceExtension class NotificationService: UANotificationServiceExtension { } ``` #### Objective-C (SPM/Carthage/xcframeworks) ```objc // NotificationService.h @import AirshipNotificationServiceExtension; @interface NotificationService : UANotificationServiceExtension @end // NotificationService.m @import Foundation; #import "NotificationService.h" @implementation NotificationService @end ``` #### Objective-C (CocoaPods) ```objc // NotificationService.h @import AirshipServiceExtension; @interface NotificationService : UANotificationServiceExtension @end // NotificationService.m @import Foundation; #import "NotificationService.h" @implementation NotificationService @end ``` That's it! The Airship base class handles downloading and attaching media from URLs in your push notifications. When you send a push notification with a media URL, the service extension will automatically download and attach the media before the notification is displayed. ## Related Documentation - [Getting Started with Push Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/getting-started/) - [Installing the Airship SDK](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) If you experience problems, see [Troubleshooting Notification Service Extensions](https://www.airship.com/docs/developer/sdk-integration/apple/troubleshooting/notification-service-extensions/) to verify setup and debug issues. # In-App Messaging > Legacy In-App Messages are banner messages delivered through push notifications that appear in-app when the user opens the application. Legacy [in-app messages](https://www.airship.com/docs/guides/messaging/messages/content/app/in-app-messages/) are delivered through push messages and automatically converted to In-App Automation (IAA) banners for display. You can customize how these messages are converted using extender blocks on the legacy in-app message manager. > **Note:** This page covers **legacy In-App Messages** delivered via push notifications. For In-App Automation (IAA) and Scenes, which are separate features with different triggers and capabilities, see [In-App Experiences](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/getting-started/). For general In-App Automation styling options, see [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/in-app-automation/). ## Modify the schedule **Modify the schedule** ```swift InAppAutomation.shared.legacyInAppMessaging.scheduleExtender = { schedule in // Modify the schedule schedule.limit = 2 } ``` ## Modify the message **Modify the message** ```swift InAppAutomation.shared.legacyInAppMessaging.messageExtender = { message in /// Modify the message if case .banner(var bannerInfo) = message.displayContent { bannerInfo.borderRadius = 10.0 message.displayContent = .banner(bannerInfo) } } ``` # Landing Pages > Landing Pages are web pages triggered from a notification response that are automatically converted to HTML In-App Automation experiences. Landing Pages are web pages triggered as an action from a notification response. When a user taps a notification with a landing page action, the landing page is automatically converted to an HTML In-App Automation experience for display. For general In-App Automation styling options, including HTML customization, see [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/in-app-automation/). ## Customize Landing Pages You can customize landing pages by registering a custom action that extends the HTML schedule before display: #### Swift ```swift Airship.actionRegistry.registerEntry( names: LandingPageAction.defaultNames ) { let action = LandingPageAction() { args, schedule in guard case .inAppMessage(var message) = schedule.data else { return } guard case .html(var htmlContent) = message.displayContent else { return } // Customize the HTML content htmlContent.forceFullscreen = true message.displayContent = .html(htmlContent) schedule.data = .inAppMessage(message) } return ActionEntry(action: action) } ``` #### Objective-C > **Note:** Custom action registration with closures is not available in Objective-C. Use Swift or subclass the action directly. # Badge Management > Manage the badge number that appears on your app icon, including manual control and automatic incrementing. The badge appears as a number on your app icon. You can set, reset, or enable auto-badge functionality. ## Get Badge Value The `badgeNumber` property returns the current badge number used by both the device and the Airship server. This property must be accessed on the main thread. #### Swift ```swift let currentBadge = await Airship.push.badgeNumber ``` #### Objective-C > **Note:** This async property is not available in Objective-C. Use `UIApplication.shared.applicationIconBadgeNumber` to read the current badge value. ## Set Badge Value Set the badge number to a specific value. This updates both the device badge and the value stored on Airship servers. #### Swift ```swift try await Airship.push.setBadgeNumber(20) ``` #### Objective-C > **Note:** This async method is not available in Objective-C. Use `UIApplication.shared.applicationIconBadgeNumber` to set the badge value directly. ## Reset Badge Reset the badge to zero on both the device and Airship servers. #### Swift ```swift try await Airship.push.resetBadge() ``` #### Objective-C > **Note:** This async method is not available in Objective-C. Set `UIApplication.shared.applicationIconBadgeNumber = 0` to reset the badge directly. ## Auto-Badge Auto-badge automatically updates the badge number stored by Airship every time the app is started or foregrounded, instead of setting an exact value. #### Swift ```swift Airship.push.autobadgeEnabled = true ``` #### Objective-C ```objc UAirship.push.autobadgeEnabled = YES; ``` > **Important:** When using auto-badge, only modify the badge value through Airship methods to ensure the value stays in sync. # Interactive Notifications > Configure standard and custom interactive notification categories with action buttons for iOS push notifications. Interactive notifications allow users to take actions directly from the notification without opening your app. You can add buttons to notifications that perform specific actions when tapped. ## Standard Interactive Notifications Airship provides built-in interactive notification types with pre-configured action buttons. See [Built-In Interactive Notification Types](https://www.airship.com/docs/reference/messages/built-in-interactive-notifications/) for available types and button configurations. To use a standard interactive notification type, specify the category ID in your push payload. The notification will automatically display the appropriate action buttons. ## Custom Interactive Notification Categories You can define custom notification categories with specific actions tailored to your app's needs. > **Note:** Airship reserves category IDs prefixed with `ua_`. Any custom categories with that prefix will be ignored. ### Create a Custom Category #### Swift ```swift // Define an action for the category let categoryAction = UNNotificationAction( identifier: "category_action", title: "Action!", options: [.authenticationRequired, .foreground, .destructive] ) // Define the category let category = UNNotificationCategory( identifier: "custom_category", actions: [categoryAction], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: "Sensitive Content Hidden", options: [] ) // Register the custom category Airship.push.customCategories = [category] ``` #### Objective-C ```objc // Define an action for the category UNNotificationAction *categoryAction = [UNNotificationAction actionWithIdentifier:@"category_action" title:@"Action!" options:(UNNotificationActionOptionForeground | UNNotificationActionOptionDestructive | UNNotificationActionOptionAuthenticationRequired)]; // Define the category UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:@"custom_category" actions:@[categoryAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone]; // Register the custom category UAirship.push.customCategories = [NSSet setWithArray:@[category]]; ``` ### Action Options When creating notification actions, you can specify the following options: - **`.foreground`** / `UNNotificationActionOptionForeground`: Opens the app when the action is tapped - **`.destructive`** / `UNNotificationActionOptionDestructive`: Displays the action button in red (use for destructive actions like "Delete") - **`.authenticationRequired`** / `UNNotificationActionOptionAuthenticationRequired`: Requires the device to be unlocked before the action can be performed ### Hidden Preview Placeholder The `hiddenPreviewsBodyPlaceholder` parameter specifies placeholder text that will be shown instead of the notification's body when the user has disabled notification previews for your app. This is useful for sensitive content that shouldn't be displayed in notification previews. ### Handle Action Responses When a user taps an action button, handle the response in your notification callback: #### Swift ```swift Airship.push.onReceivedNotificationResponse = { response in if response.actionIdentifier == "category_action" { // Handle the action } } ``` #### Objective-C ```objc UAirship.push.onReceivedNotificationResponse = ^(UNNotificationResponse *response) { if ([response.actionIdentifier isEqualToString:@"category_action"]) { // Handle the action } }; ``` ## Related Documentation - [Getting Started with Push Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/getting-started/) # App Clips > Set up App Clips with Airship to enable push notifications for lightweight app experiences. [App Clips](https://developer.apple.com/documentation/app_clips) are lightweight versions of your app that users can access quickly without installing the full app. They're designed for specific, focused tasks and can be launched via QR codes, NFC tags, links, or App Clip codes. App Clips support push notifications, specifically for transactional use cases. When a user downloads an App Clip, they're automatically opted-in to notifications for 8 hours. After this period, you can ask users to extend notification permissions for up to 1 week. ## Prerequisites > **Important:** An App Clip requires a separate application identifier in the Apple Developer Portal and a separate project in the Airship dashboard. You need to follow the same setup steps required for the main application in both the Apple Developer Portal and the [Airship dashboard](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration). ## Register an App Clip Identifier To create a push certificate for your App Clip, you need to register a new application identifier: 1. In the **Certificates, Identifiers & Profiles** section of your Apple Developer account, click **Identifiers**. 2. Click the **+** button to register a new identifier. 3. Select **App Clips** as the identifier type. ![Selecting App Clips as the identifier type](https://www.airship.com/docs/images/app-clip-identifier_hu_c7c5b6bc665a4d40.webp) *Selecting App Clips as the identifier type* 4. Specify the app ID of the parent app and the product name: ![Specifying the parent app ID and product name](https://www.airship.com/docs/images/app-clip-identifier2_hu_dd057b939ae88cc1.webp) *Specifying the parent app ID and product name* 5. Complete the registration process. ## Create an App Clip Target 1. In Xcode, click **File** → **New** → **Target...**. 2. Select **App Clip** in the **Application** section. ![Selecting the App Clip target type in Xcode](https://www.airship.com/docs/images/app-clip-target_hu_87e2d0a265a4c2c3.webp) *Selecting the App Clip target type in Xcode* 3. Configure your App Clip target and click **Finish**. 4. **Important**: Add the **Push Notifications** capability to your App Clip target: - Select your App Clip target - Go to **Signing & Capabilities** - Click **+ Capability** and add **Push Notifications** ## Configure Ephemeral Notifications By default, App Clips can receive notifications for 8 hours after installation. To enable ephemeral notifications, add the following to your App Clip's `Info.plist`: ![Ephemeral notification configuration in Info.plist](https://www.airship.com/docs/images/app-clip-plist_hu_ad54638ec83033df.webp) *Ephemeral notification configuration in Info.plist* ## Enable Extended Push Notification Permissions To send push notifications for an extended period (up to 1 week), enable extended push notification permissions: #### Swift ```swift Airship.push.extendedPushNotificationPermissionEnabled = true ``` #### Objective-C ```objc UAirship.push.extendedPushNotificationPermissionEnabled = YES; ``` ## Initialize Airship in Your App Clip Initialize the Airship SDK in your App Clip the same way you would in your main app. See the [Getting Started guide](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) for initialization instructions. ## Sending Notifications to App Clips When sending push notifications to App Clips, include the `target_content_id` in the iOS override object. This identifies the specific App Clip experience: ```json { "notification": { "ios": { "target_content_id": "https://example.com/restaurants/cafe_portland/order/1234", "alert": { "title": "Order Status", "subtitle": "Cafe Portland", "body": "Your order is ready!" } } } } ``` For more details on the iOS override object, see the [API reference](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). ## Related Documentation - [Getting Started with Push Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/getting-started/) - [Installing the Airship SDK](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) - [iOS Channel Configuration](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration) # Quiet Time > Configure quiet time to prevent notifications from being displayed during specific hours while still receiving them. Quiet time allows you to suppress notifications during specific hours. Notifications are still received but won't be displayed to the user during the quiet time window. ## Configure Quiet Time #### Swift ```swift // Set quiet time from 7:30pm to 7:30am Airship.push.setQuietTimeStartHour(19, startMinute: 30, endHour: 7, endMinute: 30) // Enable quiet time Airship.push.quietTimeEnabled = true ``` #### Objective-C ```objc // Set quiet time from 7:30pm to 7:30am [UAirship.push setQuietTimeStartHour:19 startMinute:30 endHour:7 endMinute:30]; // Enable quiet time UAirship.push.quietTimeEnabled = YES; ``` ### In-App Experiences Implement Scenes, In-App Automations, custom views, and embedded content to deliver engaging in-app experiences to your users. # In-App Experiences > Integrate Scenes & In-App Automations into your Apple app to display embedded content and create custom in-app experiences with minimal code. In-App Experiences use Airship's on-device automation framework to provide instant, personalized content that integrates natively with your app. This includes [Scenes](https://www.airship.com/docs/reference/glossary/#scene), which can be displayed as modal or fullscreen overlays or embedded directly within your app screens, and In-App Automations (IAA), which power banner, modal, and fullscreen in-app messages triggered by events. Scenes are fully customizable in the Airship dashboard and require minimal SDK integration. For advanced In-App Automation customization options, see [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/in-app-automation/). ## Requirements To use In-App Experiences, you need: - The `AirshipAutomation` module installed (see [Getting Started](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/)) - Airship SDK initialized with `takeOff` (see [Getting Started](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/)) > **Note:** **In-App Experiences work out of the box**: Once you install the `AirshipAutomation` module and initialize Airship with `takeOff`, In-App Experiences will function automatically. The rest of this documentation covers optional customization and advanced features. ## Adding custom fonts Custom fonts added to your app bundle can be used in In-App Experiences. To add fonts to your app, follow Apple's guide on [Adding a Custom Font to Your App](https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app). Once added, you'll need the font family name to use it in Airship. To find the family name: 1. Add the font files to your Xcode project 2. Use this code to print all available font family names: #### Swift ```swift for family in UIFont.familyNames.sorted() { print("Family: \(family)") for name in UIFont.fontNames(forFamilyName: family) { print(" - \(name)") } } ``` #### Objective-C ```objc for (NSString *familyName in [UIFont familyNames]) { NSLog(@"Family: %@", familyName); for (NSString *fontName in [UIFont fontNamesForFamilyName:familyName]) { NSLog(@" - %@", fontName); } } ``` After adding fonts to your app, create a Font Stack in the Airship dashboard by following the steps in [Setting brand guidelines](https://www.airship.com/docs/guides/messaging/features/brand-guidelines/). You can then select the stack when [setting In-App Experience defaults](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/defaults/) and creating in-app messages. ## Controlling display Control when and how In-App Experiences are displayed in your app. You can auto-pause displays on launch (useful for splash screens), manually pause all in-app displays, set intervals between displays, control when individual messages are ready to display, and specify which scene window displays messages in multi-scene apps. ### Auto-pausing on launch For apps with splash screens, you can configure Airship to automatically pause In-App Automation on launch. This prevents In-App Experiences from displaying during the splash screen. Once your app is ready, resume display by setting `isPaused` to `false`. Set the `autoPauseInAppAutomationOnLaunch` option in your Airship config when calling `takeOff`: #### Swift ```swift var config = AirshipConfig() // ... other config settings ... // Auto-pause on launch for splash screen config.autoPauseInAppAutomationOnLaunch = true try! Airship.takeOff(config) // Later, when splash screen is dismissed and app is ready: Airship.inAppAutomation.isPaused = false ``` #### Objective-C ```obj-c UAConfig *config = [UAConfig config]; // ... other config settings ... // Auto-pause on launch for splash screen config.autoPauseInAppAutomationOnLaunch = YES; [UAirship takeOff:config error:&airshipError]; // Later, when splash screen is dismissed and app is ready: UAirship.inAppAutomation.isPaused = NO; ``` ### Pausing display Pausing will still allow In-App Experiences to be triggered and queued up for execution, but they will not display. This is useful for preventing in-app experiences from displaying on screens where it would be detrimental to the user experience, such as splash screens, settings screens, or landing pages. #### Swift ```swift Airship.inAppAutomation.isPaused = true ``` #### Objective-C ```obj-c Airship.inAppAutomation.isPaused = YES ``` ### Display interval The display interval controls the amount of time to wait before the manager can display the next triggered In-App Experience. The default value is set to **0 seconds** and can be adjusted to any amount of time in seconds. #### Swift ```swift Airship.inAppAutomation.inAppMessaging.displayInterval = 30 ``` #### Objective-C ```obj-c UAirship.inAppAutomation.inAppMessaging.displayInterval = 30 ``` ### Controlling per-message display You can control when individual In-App Experiences are ready to display and listen for when they are displayed or finished. This is useful when you need to check app state before displaying content, such as: - Verifying the current screen or view controller is appropriate for the message - Checking custom data in the message's extras (custom keys) to determine if it should display - Ensuring certain app conditions are met before showing the message - Integrating with other in-app messaging products #### Swift Set a closure on to control the display. You have access to the message and schedule ID: ```swift Airship.inAppAutomation.inAppMessaging.onIsReadyToDisplay = { message, scheduleID in // Return false to prevent display return false } ``` `onIsReadyToDisplay` will be called whenever state in the app changes (screen, app state, message finished displaying, etc...), you can also trigger it manually with `notifyDisplayConditionsChanged`: ```swift Airship.inAppAutomation.inAppMessaging.notifyDisplayConditionsChanged() ``` #### Objective-C Implement the `InAppMessageDisplayDelegate` to control the display. You have access to the message and schedule ID: **Implement the InAppMessageDisplayDelegate** ```obj-c - (BOOL)isMessageReadyToDisplay:(UAInAppMessage *)message scheduleID:(NSString *)scheduleID { // Return NO to prevent display return NO; } ``` **Set the delegate** ```obj-c UAirship.inAppAutomation.inAppMessaging.displayDelegate = self; ``` `isMessageReadyToDisplay` will be called whenever state in the app changes (screen, app state, message finished displaying, etc...), you can also trigger it manually with `notifyDisplayConditionsChanged`: ```obj-c [UAirship.inAppAutomation.inAppMessaging notifyDisplayConditionsChanged]; ``` ### Displaying in multiple scene apps By default, In-App Experiences are displayed in the last active window scene. The [InAppMessageSceneDelegate](https://urbanairship.github.io/ios-library/v20/AirshipAutomation/documentation/airshipautomation/inappmessagescenedelegate) allows you to override this behavior and control which UIWindowScene displays a given in-app message. Use this delegate when your app supports multiple scenes and you need to customize which scene displays the message. #### Swift **Implement the InAppMessageSceneDelegate** ```swift func sceneForMessage(_ message: InAppMessage) -> UIWindowScene? { // return a custom scene or nil to use default } ``` **Set the delegate** ```swift Airship.inAppAutomation.inAppMessaging.sceneDelegate = sceneDelegate ``` #### Objective-C **Implement the InAppMessageSceneDelegate** ```obj-c - (UIWindowScene *)sceneForMessage:(UAInAppMessage *)message { // return a custom scene or nil to use default return nil; } ``` **Set the delegate** ```obj-c UAirship.inAppAutomation.inAppMessaging.sceneDelegate = self; ``` ## Next steps - Learn how to [create Scenes in the Airship dashboard](https://www.airship.com/docs/guides/messaging/in-app-experiences/scenes/create/) - Present Scene content with [Embedded Content](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/embedded-content/) - Create reusable components with [Custom Views](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/custom-views/) - Customize [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/in-app-automation/) for IAA # Embedded Content > Integrate Embedded Content into your iOS app to display Scene content directly within your app's screens. For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see [Embedded Content](https://www.airship.com/docs/guides/features/messaging/scenes/embedded-content/). ## Adding an embedded view The `AirshipEmbeddedView` is a SwiftUI view that defines a place for an Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `embeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```swift // Show any "home_banner" Embedded Content AirshipEmbeddedView(embeddedID: "home_banner") ``` ## Placeholders If content is unavailable to display, the default behavior is to show an `EmptyView`. You can customize this by providing a placeholder. **Basic integration with placeholder** ```swift AirshipEmbeddedView(embeddedID: "home_banner") { Text("Placeholder!") } ``` ## Placing in a scroll view When placed directly in a `ScrollView`, or a child view within the `ScrollView` that is allowed to grow unbounded in the scrollable direction, you need to pass the maximum size of the embedded view to make percent-based sizing work correctly. The easiest way is to wrap the `ScrollView` in a `GeometryReader` and pass the size info to the embedded view. **GeometryReader example** ```swift struct ScrollViewExample: View { var body: some View { GeometryReader { geometryProxy in ScrollView(showsIndicators: false) { AirshipEmbeddedView( embeddedID: "home_banner", embeddedSize: AirshipEmbeddedSize( parentBounds: geometryProxy.size ) ) } } } } ``` When using a `GeometryReader`, it takes up as much space as allowed. To avoid this and instead measure the current size of the content, you can use the view extension `airshipMeasureView`. **airshipMeasureView example** ```swift struct ScrollViewExample: View { @State var state: CGSize? var body: some View { ScrollView(showsIndicators: false) { AirshipEmbeddedView( embeddedID: "home_banner", embeddedSize: AirshipEmbeddedSize( maxWidth: state?.width, maxHeight: state?.height ) ) } .airshipMeasureView(self.$state) } } ``` ## Styling You can set a custom style on the embedded view, which allows you to modify how the content is displayed or what pending content is displayed. In this example, the embedded view has a Dismiss Button above it: **Custom style** ```swift public struct CustomEmbeddedViewStyle: AirshipEmbeddedViewStyle { @ViewBuilder public func makeBody(configuration: AirshipEmbeddedViewStyleConfiguration) -> some View { if let view = configuration.views.first { VStack { Button("Dismiss") { view.dismiss() } view } } else { configuration.placeHolder } } } ``` **Setting the style** ```swift AirshipEmbeddedView(embeddedID: "home_banner") .setAirshipEmbeddedStyle(CustomEmbeddedViewStyle()) ``` ## Observing available embedded content Embedded Content is not always available, and even after being triggered, it still needs to be prepared before it can be displayed. An `AirshipEmbeddedView` will automatically update when content is available and transition from the placeholder to the content once content is available. If you need to query the availability of Embedded Content, you can use an `AirshipEmbeddedObserver` to watch for updates. An `AirshipEmbeddedObserver` is an `ObservableObject` that you can use as a `StateObject` to automatically refresh the view when new Embedded Content is available. It allows for more dynamic handling of Embedded Content than just content or a placeholder. **Observable example** ```swift struct ObservableExample: View { @StateObject private var embeddedObserver: AirshipEmbeddedObserver = AirshipEmbeddedObserver(embeddedID: "home_banner") @State var tabIndex = 0 var body: some View { if (embeddedObserver.embeddedInfos.isEmpty) { Text("No banner available") } else { Text("Banner available") AirshipEmbeddedView(embeddedID: "home_banner") } } } ``` The `AirshipEmbeddedObserver` can be created to watch for one `embeddedID`, all embedded IDs, or use custom filtering for embedded IDs. The `embeddedInfos` is the FIFO order of embedded info, including the extras you can set through the Scene composer when creating the content. # Custom Views > Register custom SwiftUI views with the AirshipCustomViewManager to use them in Scenes. ![Custom View rendered in a Scene on iOS](https://www.airship.com/docs/images/custom-views-apple_hu_32d8fbb68fdc44c2.webp) *Custom View rendered in a Scene on iOS* To use Custom Views, you must first register the view's name with the `AirshipCustomViewManager`. The name is referenced when adding the Custom View to a Scene. The view manager will call through to the view builders registered for that view's name and provide the properties, name, and some layout hints as arguments. All Custom Views should be registered [after takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/). #### Swift ```swift struct CustomViewProperties: Decodable { var text: String } AirshipCustomViewManager.shared.register(name: "custom_view") { args in let viewProperties: CustomViewProperties = if let decoded = try? args.properties?.decode() { decoded } else { CustomViewProperties(text: "fallback") } Text(viewProperties.text) } ``` ## Example custom view The following example shows a Custom View that renders an embedded map when called to render a Custom View named `map`. In our example, we have `properties` that defines a single `place` field, which is the address of the location that the map should render. #### Swift First, define the view and its properties: ```swift import SwiftUI import MapKit import CoreLocation struct CustomMapView: View { struct Args: Decodable { let place: String } let args: Args @State private var region: MKCoordinateRegion? @State private var pinCoordinate: CLLocationCoordinate2D? var body: some View { if let region = region, let pinCoordinate = pinCoordinate { Map(coordinateRegion: .constant(region), annotationItems: [MapPin(coordinate: pinCoordinate)]) { pin in MapMarker(coordinate: pin.coordinate, tint: Color.red) } } else { Text("Loading map...") .onAppear { geocodePlace() } } } private func geocodePlace() { let geocoder = CLGeocoder() geocoder.geocodeAddressString(args.place) { placemarks, error in if let placemark = placemarks?.first, let location = placemark.location { let coordinate = location.coordinate self.region = MKCoordinateRegion( center: coordinate, span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) ) self.pinCoordinate = coordinate } else { print("Failed to geocode place: \(error?.localizedDescription ?? "Unknown error")") } } } } struct MapPin: Identifiable { let id = UUID() let coordinate: CLLocationCoordinate2D } struct CustomMapView_Previews: PreviewProvider { static var previews: some View { CustomMapView(args: CustomMapView.Args(place: "Eiffel Tower")) } } ``` Then register the view after takeOff: ```swift AirshipCustomViewManager.shared.register(name: "map", builder: { args in if let args: CustomMapView.Args = try? args.properties?.decode() { CustomMapView(args: args) } }) ``` ## Scene Control Custom views control their parent scene through the `AirshipSceneController`, which is automatically injected as an `@EnvironmentObject`. ### Accessing SceneController Declare the scene controller as an `@EnvironmentObject` property in your custom view. #### Swift ```swift struct CustomMapView: View { @EnvironmentObject var sceneController: AirshipSceneController let args: Args var body: some View { // Your custom view content } } ``` ### Dismissing scenes Call `dismiss()` to close the scene, or set `cancelFutureDisplays` to prevent it from displaying again. #### Swift ```swift struct CustomMapView: View { @EnvironmentObject var sceneController: AirshipSceneController var body: some View { VStack { // Map display code... Button("Close Map") { sceneController.dismiss() } Button("Got It") { sceneController.dismiss(cancelFutureDisplays: true) } } } } ``` ### Pager navigation Navigate between pages using the `pager` controller's `canGoBack` and `canGoNext` properties. #### Swift ```swift struct CustomMapView: View { @EnvironmentObject var sceneController: AirshipSceneController var body: some View { HStack { if sceneController.pager.canGoBack { Button("Back") { sceneController.pager.navigate(request: .back) } } if sceneController.pager.canGoNext { Button("Next Location") { sceneController.pager.navigate(request: .next) } } } } } ``` ### Sizing management Use `args.sizeInfo` to determine appropriate sizing for your custom view. #### Swift ```swift AirshipCustomViewManager.shared.register(name: "map") { args in if let args: CustomMapView.Args = try? args.properties?.decode() { CustomMapView(args: args) .frame( width: args.sizeInfo.isAutoWidth ? 350 : nil, height: args.sizeInfo.isAutoHeight ? 500 : nil ) } } ``` ![Map Custom View in a Scene on iOS](https://www.airship.com/docs/images/custom-views-map-scene-apple_hu_46a1471648ebe081.webp) *Map Custom View in a Scene on iOS* ## Embedding Airship Views Airship views like Preference Center can be embedded as custom views. #### Swift ```swift // Full preference center with navigation bar AirshipCustomViewManager.shared.register(name: "preference_center") { args in let id = args.properties?.object?["id"]?.string ?? "default" return PreferenceCenterView(preferenceCenterID: id) .frame( maxWidth: args.sizeInfo.isAutoWidth ? nil : .infinity, maxHeight: args.sizeInfo.isAutoHeight ? nil : .infinity ) } // Content only (no navigation bar) AirshipCustomViewManager.shared.register(name: "preference_center_content") { args in let id = args.properties?.object?["id"]?.string ?? "default" return PreferenceCenterContent(preferenceCenterID: id) .frame( maxWidth: args.sizeInfo.isAutoWidth ? nil : .infinity, maxHeight: args.sizeInfo.isAutoHeight ? nil : .infinity ) } ``` ![Preference Center Custom View in a Scene on iOS](https://www.airship.com/docs/images/custom-views-preference-center-scene-apple_hu_6e4d0728f598b316.webp) *Preference Center Custom View in a Scene on iOS* # In-App Automation > Integrate options for In-App Automation (IAA) customization. In-App Automation (IAA) powers banner, modal, and fullscreen in-app messages. ## Customization Various options are available for customizing the container view for In-App Automation content via the native SDKs. Scenes are fully customizable in the dashboard and cannot be customized via the SDK. ## Styles Plists can be used to modify any of the default message styles that the SDK provides. Each message type can be customized with a different plist: - **Banner**: `UAInAppMessageBannerStyle.plist` - **HTML**: `UAInAppMessageHTMLStyle.plist` - **FullScreen**: `UAInAppMessageFullScreenStyle.plist` - **Modal**: `UAInAppMessageModalStyle.plist` These plists support the following values: - **Banner** - `additionalPadding`: _Padding_. Adds padding around the view. - `headerStyle`: _Text Style_. Customizes the message's header. - `bodyStyle`: _Text Style_. Customizes the message's body. - `mediaStyle`: _Media Style_. Customizes the message's media. - `buttonStyle`: _Buttons Style_. Customizes the message's buttons. - `maxWidth`: _Points_. Max width. - `tapOpacity`: _Tap Opacity_. Customizes the message's opacity it's tapped and a tap action is present. - `shadowStyle`: _Shadow Style_. Customizes the message's shadow. - **FullScreen** - `headerStyle`: _Text Style_. Customizes the banner's header. - `bodyStyle`: _Text Style_. Customizes the banner's body. - `mediaStyle`: _Media Style_. Customizes the banner's media. - `buttonStyle`: _Buttons Style_. Customizes the banner's buttons. - `dismissIconResource`: String. Resource name for a custom dismiss icon. - **Modal** - `additionalPadding`: _Padding_. Adds padding around the view. - `headerStyle`: _Text Style_. Customizes the banner's header. - `bodyStyle`: _Text Style_. Customizes the banner's body. - `mediaStyle`: _Media Style_. Customizes the banner's media. - `buttonStyle`: _Buttons Style_. Customizes the banner's buttons. - `dismissIconResource`: String. Resource name for a custom dismiss icon. - `maxWidth`: _Points_. Max width. - `maxHeight`: _Points_. Max height. - `extendFullScreenLargeDevice`: _Boolean_. True to allow the option 'Display fullscreen on small screen device' to extend to large devices as well. - **HTML** - `additionalPadding`: _Padding_. Adds padding around the view. - `dismissIconResource`: String. Resource name for a custom dismiss icon. - `maxWidth`: _Points_. Max width. - `maxHeight`: _Points_. Max height. - `extendFullScreenLargeDevice`: _Boolean_. True to allow the option 'Display fullscreen on small screen device' to extend to large devices as well. - **Padding** - `top`: _Points_. Top padding. - `bottom`: _Points_. Bottom padding. - `leading`: _Points_. Leading padding. - `trailing`: _Points_. Trailing padding. - **Buttons Style** - `additionalPadding`: _Padding_. Adds padding around the button area. - `buttonHeight`: _Points_. Button height. - `stackedButtonSpacing`: _Points_. Button spacing in the stacked layout. - `separatedButtonSpacing`: _Points_. Button spacing in the separated layout. - `borderWidth`: _Points_. Button's border width. - `buttonTextStyle`: _Text Style_. Text style for each button. - **Text Style** - `additionalPadding`: _Padding_. Adds padding around the view. - `letterSpacing`: _Points_. Spacing between the letters. - `lineSpacing`: _Points_. Spacing between lines. - **Media Style** - `additionalPadding`: _Padding_. Adds padding around the view. - **Shadow Style** - `colorHex`: _Color_. Shadow color. - `radius`: _Points_. Shadow radius. - `xOffset`: _Points_. Shadow x-axis offset. - `yOffset`: _Points_. Shadow y-axis offset. ## Fonts You can use custom fonts in your in-app messages by adding them to your app bundle and configuring them in the Airship dashboard. ### Custom Fonts Fonts added to the app bundle are available for use with in-app messaging. To add fonts, please read the [The UIKit Custom Fonts Guide](https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app). ![iOS Custom Font](https://www.airship.com/docs/images/ios/ios-custom-font_hu_abb515eea5dfec6b.webp) *iOS Custom Font* After adding fonts to your app, create a Font Stack in the Airship dashboard by following the steps in [Setting brand guidelines](https://www.airship.com/docs/guides/messaging/features/brand-guidelines/). Then you can select the stack when [setting in-app message defaults](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/defaults/) and creating in-app messages. ### Dynamic fonts With HTML in-app messages Most In-App message styles support automatically scaling fonts through the use of Dynamic Type. However, automatically scaling fonts in HTML In-App messages requires you to use the following Apple system fonts when specifying the CSS font property: - `-apple-system-body` - `-apple-system-headline` - `-apple-system-subheadline` - `-apple-system-caption1` - `-apple-system-caption2` - `-apple-system-footnote` - `-apple-system-short-body` - `-apple-system-short-headline` - `-apple-system-short-subheadline` - `-apple-system-short-caption1` - `-apple-system-short-footnote` - `-apple-system-tall-body` For example, to have the HTML body default to the Apple system font body style: ```html body { font: -apple-system-body; // available on Apple devices only } ``` For more information about dynamic type, please see this [WWDC video](https://developer.apple.com/videos/play/wwdc2017/245/). ## Customizing HTML In-App Messages > **Note:** In order for the Airship JavaScript interface to be loaded into the webview, the URL must be specified in the URL Allowlist. See [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/apple/installation/advanced-integration/#url-allowlist) for configuration details. HTML in-app messages provide a way to display custom content inside a native web view. These types of in-app messages display with a dismiss button built in, but can also be customized to provide their own buttons capable of dismissing the view. Dismissing a view requires calling the dismiss function on the UAirship JavaScript interface with a button resolution object passed in as a parameter. The button resolution object is a JSON object containing information about the interaction type and the button performing the dismissal. It should match the following format: ```javascript { "type" : "button_click", "button_info" : { "id" : "button identifier", "label" : {"text": "foo"} } } ``` The button resolution requires each of the key fields shown above. These include: - `type` — The type key with the value of resolution type `button_click` - `button_info` — The button info object containing required id and label fields - `id` — The button identifier - `label` — Label object containing the required text key - `text` — The text key with a string value representing the label text Providing a basic dismiss button in HTML: ```html ``` ## Custom adapters Providing an adapter allows defining the behavior of the custom type or overriding any of the default message types. The adapter will be created by the in-app messaging manager when a message's schedule is triggered. Once created, the adapter can define when the message is ready to display and the display behavior. After the message is displayed, the caller of the display method must be notified that the message is finished displaying by returning a `CustomDisplayResolution` when finished. This will allow for subsequent in-app messages to be displayed. **Example custom banner adapter** ```swift final class CustomBannerAdapter: CustomDisplayAdapter { private let message: InAppMessage private let assets: AirshipCachedAssetsProtocol init(message: InAppMessage, assets: any AirshipCachedAssetsProtocol) { self.message = message self.assets = assets } @MainActor var isReady: Bool { get { /// Called before display return true } } @MainActor func waitForReady() async { // If `isReady` is false this will be called to wait for the // adapter to be ready } @MainActor func display(scene: UIWindowScene) async -> CustomDisplayResolution { return await withCheckedContinuation { continuation in /// After displaying call the continuation with the result continuation.resume(returning: .userDismissed) } } } ``` **Register a factory block to return the adapter** ```swift /// Set the factory block after takeOff InAppAutomation.shared.inAppMessaging.setAdapterFactoryBlock(forType: .banner) { message, assets in return CustomBannerAdapter(message: message, assets: assets) } ``` ### Message Center Implement Message Center to provide an inbox for rich HTML-based messages, including display, theming, embedding, and advanced customization. # Message Center > Message Center provides an inbox for rich HTML-based messages that users can view at their convenience, with support for custom theming and display handling. ![Message Center inbox on iOS](https://www.airship.com/docs/images/message-center-apple.webp) *Message Center inbox on iOS* Message Center provides an inbox for rich, HTML-based messages. Learn more about Message Center in our [feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Display the Message Center Display the Message Center with a single method call: #### Swift ```swift Airship.messageCenter.display() ``` #### Objective-C ```objc [UAirship.messageCenter display]; ``` This displays the Message Center as an overlay window, allowing users to view and manage their messages. When the user closes the Message Center, any changes (such as marking messages as read) are automatically synced with Airship. > **Note:** To embed the Message Center directly in your app's navigation instead of displaying it as an overlay, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/embedding/). You can also [intercept display requests](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/embedding/#handling-display-requests) to handle navigation to your embedded Message Center. ## Applying a Custom Theme You can customize the appearance of the Message Center by creating a `MessageCenterTheme` instance and setting its properties. The theme applies globally to all Message Centers displayed in your app. ### Setting the Theme Programmatically (Swift) #### Swift ```swift var theme = MessageCenterTheme() theme.cellTitleFont = .title theme.cellDateFont = .body theme.cellTitleColor = .primary theme.cellDateColor = .secondary theme.unreadIndicatorColor = .blue // Set the theme on the Message Center Airship.messageCenter.theme = theme ``` ### Setting the Theme from a Plist You can also customize the theme without writing code by creating a plist file. All keys in the plist correspond to properties on the `MessageCenterTheme` class. **Color Format:** - **Named colors**: Must correspond to a named color defined in a color asset within the main bundle - **Hexadecimal colors**: Use separate keys for light/dark mode (e.g., `cellTitleColor` and `cellTitleColorDark`) > **Note:** If your app is written in Objective-C, you must use the plist file to customize your theme, as `MessageCenterTheme` is a Swift struct. Save the plist as `MessageCenterTheme.plist` in your app bundle. #### Example Theme Plist **MessageCenterTheme.plist** ```xml refreshTintColor #333333 refreshTintColorDark #DDDDDD iconsEnabled placeholderIcon placeholderIcon cellTitleFont fontName ChalkboardSE-Regular fontSize 16 cellDateFont fontName ChalkboardSE-Regular fontSize 14 cellColor #DDDDDD cellColorDark #333333 cellTitleColor #000000 cellTitleColorDark #FFFFFF cellDateColor #222222 cellDateColorDark #CCCCCC cellSeparatorStyle none cellSeparatorColor #FFFFFF cellSeparatorColorDark #000000 cellTintColor #FF0000 cellTintColorDark #00FF00 unreadIndicatorColor #FF0000 unreadIndicatorColorDark #FF0000 selectAllButtonTitleColor #333333 selectAllButtonTitleColorDark #DDDDDD deleteButtonTitleColor #333333 deleteButtonTitleColorDark #DDDDDD markAsReadButtonTitleColor #333333 markAsReadButtonTitleColorDark #DDDDDD hideDeleteButton editButtonTitleColor #333333 editButtonTitleColorDark #DDDDDD cancelButtonTitleColor #333333 cancelButtonTitleColorDark #DDDDDD backButtonColor #333333 backButtonColorDark #DDDDDD navigationBarTitle Nav Bar Title ``` ## Working with Messages The Message Center provides methods to fetch, mark as read, and delete messages programmatically. ### Fetch Messages Retrieve messages from the inbox: #### Swift ```swift let messages = await Airship.messageCenter.inbox.messages ``` #### Objective-C ```objc [UAirship.messageCenter.inbox getMessagesWithCompletionHandler:^(NSArray *messages) { // Handle messages }]; ``` ### Listen for Message Updates Subscribe to message updates using Combine publishers: #### Swift ```swift Airship.messageCenter.inbox.messagePublisher .receive(on: RunLoop.main) .sink(receiveValue: { messages in // Update your UI with the new messages self.messages = messages }) .store(in: &self.subscriptions) ``` #### Objective-C ```objc // Not available in Objective-C. Use KVO or polling instead. ``` ### Listen for Unread Count Changes Subscribe to unread count updates: #### Swift ```swift Airship.messageCenter.inbox.unreadCountPublisher .receive(on: RunLoop.main) .sink { unreadCount in // Update badge or UI self.unreadCount = unreadCount } .store(in: &self.subscriptions) ``` #### Objective-C ```objc // Not available in Objective-C. Use KVO or polling instead. ``` ### Refresh Messages Manually refresh the message list from the server: #### Swift ```swift let refreshed = await Airship.messageCenter.inbox.refreshMessages() ``` #### Objective-C ```objc [UAirship.messageCenter.inbox refreshMessagesWithCompletionHandler:^(BOOL result) { // Handle result }]; ``` ### Mark Messages as Read Mark one or more messages as read: #### Swift ```swift await Airship.messageCenter.inbox.markRead(messageIDs: [messageID]) ``` #### Objective-C ```objc [UAirship.messageCenter.inbox markReadWithMessageIDs:@[messageID] completionHandler:^{ // Marked read }]; ``` ### Delete Messages Delete one or more messages: #### Swift ```swift await Airship.messageCenter.inbox.delete(messageIDs: [messageID]) ``` #### Objective-C ```objc [UAirship.messageCenter.inbox deleteWithMessageIDs:@[messageID] completionHandler:^{ // Deleted }]; ``` ## Filter Messages by Named User By default, Message Center displays all messages sent to the device's channel. If multiple users log into your app on the same device, they'll all see the same messages. To filter messages by named user, set up filtering in your custom Message Center implementation. See [Message Center Filtering](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/embedding/#message-center-filtering) in the Embedding guide. When creating Message Center messages, include a custom key with `named_user_id` as the key and the user's actual ID as the value: - **For the API**: Use the `extra` object in the [Message Center object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#messageobject). - **In the dashboard**: See [Add custom keys](https://www.airship.com/docs/guides/messaging/messages/content/app/message-center/#add-custom-keys) in the Message Center content guide. ### Filtering Behavior With named user filtering enabled: - If you target `User A` in a message while they are logged in, the message appears in their inbox. - If you target `User B` in a message while they are logged in, the message appears in their inbox. - If you target `User A` or `User B` while the other is logged in, the message does not appear. - If you target `User A` or `User B` while neither is logged in, the message does not appear. # Embed the Message Center > Customize the Message Center appearance with SwiftUI view styles, create custom implementations with UIKit, and filter messages by named user. By default, Airship displays the Message Center as an overlay on top of your app. For tighter integration with your app's navigation flow, you can embed the Message Center views directly into your app. ## Prerequisites Message Center requires the `AirshipMessageCenter` module. See the [SDK installation guide](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) for setup instructions. ## Embedding the Message Center View The `MessageCenterView` (Swift) provides a complete Message Center with a built-in navigation stack. You can choose between different navigation styles for optimal display on iPhone and iPad. ### Using MessageCenterView with Navigation #### Swift ```swift import SwiftUI import AirshipMessageCenter struct MyMessageCenterScreen: View { var body: some View { // Default navigation style (adaptive) MessageCenterView() } } ``` #### Objective-C ```objc // Not supported. Use display callbacks to show custom UI (see below). ``` ### Navigation Styles `MessageCenterView` supports different navigation styles via the `navigationStyle` parameter: #### Swift ```swift // Auto navigation (default - adaptive based on device) MessageCenterView(navigationStyle: .auto) // Stack navigation (single column) MessageCenterView(navigationStyle: .stack) // Split navigation (master-detail for iPad) MessageCenterView(navigationStyle: .split) ``` - **`.auto`** (default): Automatically uses split view on iPad and stack view on iPhone - **`.stack`**: Single-column navigation for all devices - **`.split`**: Two-column master-detail layout for all devices ### Content View Without Navigation Stack For full control over the navigation, use `MessageCenterContent` (Swift only) which provides just the content without a built-in navigation stack. This requires a `MessageCenterController` to manage the message list state. #### Swift ```swift import SwiftUI import AirshipMessageCenter struct MyMessageCenterScreen: View { @StateObject private var controller = MessageCenterController() var body: some View { NavigationStack { MessageCenterContent(controller: controller) .navigationTitle("Messages") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Settings") { // Custom action } } } } } } ``` You can also provide a predicate to filter messages: #### Swift ```swift MessageCenterContent( controller: controller, predicate: CustomPredicate() ) ``` > **Note:** To apply a custom theme globally, see [Getting Started: Applying a Custom Theme](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/getting-started/#applying-a-custom-theme). ### Using MessageCenterController with NavigationView (Legacy) For apps that need to support older iOS versions or integrate with `NavigationView`, you can use `MessageCenterController` to manually manage navigation state: #### Swift ```swift import SwiftUI import AirshipMessageCenter struct MyMessageCenterScreen: View { @StateObject private var messageCenterController = MessageCenterController() var body: some View { NavigationView { ZStack { MessageCenterContent(controller: self.messageCenterController) NavigationLink( destination: Group { if case .message(let messageID) = self.messageCenterController.path.last { MessageCenterMessageViewWithNavigation(messageID: messageID) { // Clear selection on close self.messageCenterController.path.removeAll() } } else { EmptyView() } }, isActive: Binding( get: { self.messageCenterController.path.last != nil }, set: { isActive in if !isActive { self.messageCenterController.path.removeAll() } } ) ) { EmptyView() } .hidden() } } } } ``` This pattern is also useful for UIKit integration where you need manual control over navigation state. ## Customizing View Styles (Swift Only) You can customize the appearance of the Message Center by creating custom styles for the view wrapper and message list. ### Custom Message Center View Style Create a custom style that implements `MessageCenterViewStyle` to control the navigation wrapper around the Message Center content: #### Swift ```swift struct CustomMessageCenterViewStyle: MessageCenterViewStyle { @ViewBuilder func makeBody(configuration: Configuration) -> some View { if #available(iOS 16.0, *) { NavigationStack { configuration.content .navigationBarTitleDisplayMode(.inline) .navigationTitle(Text("Custom Message Center")) .toolbarBackground(.mint, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) .toolbarColorScheme(configuration.colorScheme) } } else { NavigationView { configuration.content .navigationTitle(Text("Custom Message Center")) .navigationBarTitleDisplayMode(.large) } .navigationViewStyle(.stack) } } } ``` Apply the custom style: #### Swift ```swift MessageCenterView() .messageCenterViewStyle(CustomMessageCenterViewStyle()) ``` ### Customize Item and Message Views Customize the Message Center item view and message view: #### Swift ```swift MessageCenterView() .messageCenterTheme(theme) .setMessageCenterItemViewStyle(messageCenterListItemViewStyle) .setMessageCenterMessageViewStyle(messageViewStyle) ``` ## Message Center Filtering Filter messages using a predicate. Only messages that match the predicate will be displayed. ### Filter by Named User Filter messages to show only those for the current named user: #### Swift ```swift class NamedUserPredicate: MessageCenterPredicate { func evaluate(message: MessageCenterMessage) -> Bool { guard let namedUserID = Airship.contact.namedUserID else { return false } // Check if message has matching named_user_id in extras if let extras = message.extras, let messageNamedUserID = extras["named_user_id"] as? String { return messageNamedUserID == namedUserID } return false } } Airship.messageCenter.predicate = NamedUserPredicate() ``` #### Objective-C ```objc // Not supported. Filtering requires Swift implementation. ``` ### Custom Filtering Create custom predicates for any filtering logic: #### Swift ```swift class CustomPredicate: MessageCenterPredicate { func evaluate(message: MessageCenterMessage) -> Bool { // Example: Only show messages with "cool" in the title return message.title.contains("cool") } } Airship.messageCenter.predicate = CustomPredicate() ``` #### Objective-C ```objc // Not supported. Filtering requires Swift implementation. ``` If you're embedding `MessageCenterView` directly, pass the predicate through the view modifier: #### Swift ```swift MessageCenterView( controller: MessageCenterController() ) .messageCenterPredicate(CustomPredicate()) ``` ## Custom Message Center Implementation For complete control over Message Center placement and navigation, create a custom implementation using the Message Center components. ### Key Components **MessageCenter** : The main entry point for fetching messages and handling callbacks. Access via `Airship.messageCenter`. **MessageCenterInboxProtocol** : Provides an interface for retrieving messages asynchronously and accessing the local message array. > **Note:** The message list uses CoreData. Message objects are ephemeral references refreshed with the list. Don't hold onto individual message instances indefinitely. **MessageCenterMessage** : Model object representing an individual message. Instances don't contain the message body—they point to authenticated URLs that should be displayed in a webview. **Display Callbacks** : Set `Airship.messageCenter.onDisplay` to handle when messages should be displayed, and `Airship.messageCenter.onDismissDisplay` to handle dismiss events. **NativeBridge** : For custom webview implementations, set a `NativeBridge` instance as the navigation delegate on your `WKWebView` to enable JavaScript bridge functionality. ### Handling Display Requests {#handling-display-requests} Set display callbacks after `takeOff` to handle Message Center display events: #### Swift ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Call takeOff try! Airship.takeOff(config, launchOptions: launchOptions) // Set Message Center display callback Airship.messageCenter.onDisplay = { messageID in // Navigate to your custom Message Center UI // messageID is optional - nil means show the full list // Return true to prevent default SDK display return true } // Set Message Center dismiss callback Airship.messageCenter.onDismissDisplay = { // Dismiss your custom Message Center UI } return true } ``` #### Objective-C ```objc - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Call takeOff [UAirship takeOff:config launchOptions:launchOptions error:nil]; // Set Message Center display callback UAirship.messageCenter.onDisplay = ^BOOL(NSString * _Nullable messageID) { // Navigate to your custom Message Center UI // messageID is optional - nil means show the full list // Return YES to prevent default SDK display return YES; }; // Set Message Center dismiss callback UAirship.messageCenter.onDismissDisplay = ^{ // Dismiss your custom Message Center UI }; return YES; } ``` ## Badge Updates Update the app badge to reflect the Message Center unread count: #### Swift ```swift Task { UIApplication.shared.applicationIconBadgeNumber = await Airship.messageCenter.inbox.unreadCount } ``` #### Objective-C ```objc [UAirship.messageCenter.inbox getUnreadCountWithCompletionHandler:^(NSInteger unreadCount) { dispatch_async(dispatch_get_main_queue(), ^{ UIApplication.sharedApplication.applicationIconBadgeNumber = unreadCount; }); }]; ``` > **Important:** If you use this method, don't include badge values in push notification payloads, as the Message Center will override them. ### Preference Center Implement Preference Centers to allow users to manage their subscription preferences, including display, theming, and embedding options. # Preference Center > Display Preference Centers using the Airship UI, which automatically handles user preferences and syncs with the Airship backend. ![Preference Center on iOS](https://www.airship.com/docs/images/preference-center-apple_hu_615c93ae107ba1cb.webp) *Preference Center on iOS* > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. The Preference Center allows users to opt in and out of subscription lists configured in the Airship Dashboard. The `AirshipPreferenceCenter` module provides a complete, ready-to-use UI that displays over your app. For more information about configuring Preference Centers, see the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Displaying a Preference Center Display a Preference Center with a single method call. The Preference Center will appear in its own window over your app with the provided Airship UI. #### Swift ```swift Airship.preferenceCenter.display("my-first-pref-center") ``` #### Objective-C ```objc [UAirship.preferenceCenter display:@"my-first-pref-center"]; ``` This displays the Preference Center as an overlay window, allowing users to manage their subscription preferences. When the user closes the Preference Center, any changes are automatically synced with Airship. > **Note:** To embed the Preference Center directly in your app's navigation instead of displaying it as an overlay, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/apple/preference-center/embedding/). You can also [intercept display requests](https://www.airship.com/docs/developer/sdk-integration/apple/preference-center/embedding/#handling-display-requests) to handle navigation to your embedded Preference Center. ## Applying a Custom Theme You can customize the appearance of the Preference Center by creating a `PreferenceCenterTheme` instance and setting its properties. The theme applies globally to all Preference Centers displayed in your app. ### Setting the Theme Programmatically (Swift) #### Swift ```swift // Customize your Theme var theme = PreferenceCenterTheme() theme.viewController = PreferenceCenterTheme.ViewController( navigationBar: PreferenceCenterTheme.NavigationBar( title: "My preference center", backgroundColor: .orange ) ) theme.preferenceCenter = PreferenceCenterTheme.PreferenceCenter( subtitleAppearance: PreferenceCenterTheme.TextAppearance( font: .subheadline, color: .yellow ), retryButtonBackgroundColor: .green, retryButtonLabelAppearance: PreferenceCenterTheme.TextAppearance( font: .title3, color: .black ) ) theme.contactSubscription = PreferenceCenterTheme.ContactSubscription( titleAppearance: PreferenceCenterTheme.TextAppearance( font: .title, color: .red ), subtitleAppearance: PreferenceCenterTheme.TextAppearance( font: .title2, color: .yellow ) ) theme.channelSubscription = PreferenceCenterTheme.ChannelSubscription( titleAppearance: PreferenceCenterTheme.TextAppearance( font: .title, color: .red ), subtitleAppearance: PreferenceCenterTheme.TextAppearance( font: .title2, color: .yellow ) ) // Set the Theme on the Preference Center Airship.preferenceCenter.theme = theme ``` ### Setting the Theme in SwiftUI In SwiftUI, you can apply a theme directly to the `PreferenceCenterView`: #### Swift ```swift PreferenceCenterView( preferenceCenterID: "preferenceCenter-ID" ) .preferenceCenterTheme(theme) ``` ### Setting the Theme from a Plist You can also customize the theme without writing code by creating a plist file. All keys in the plist correspond to properties on the `PreferenceCenterTheme` class. Colors are represented by strings, either a valid color hexadecimal (e.g., `#FF0000`) or a named color. Named color strings must correspond to a named color defined in a color asset within the main bundle. > **Note:** If your app is written in Objective-C, you must use the plist file to customize your theme, as `PreferenceCenterTheme` is a Swift struct. Save the plist as `AirshipPreferenceCenterTheme.plist` in your app bundle, then load it: #### Swift ```swift try Airship.preferenceCenter.setThemeFromPlist("AirshipPreferenceCenterTheme") ``` #### Objective-C ```objc NSError *error = nil; [UAirship.preferenceCenter setThemeFromPlist:@"AirshipPreferenceCenterTheme" error:&error]; if (error) { NSLog(@"Failed to set theme: %@", error); } ``` #### Example Theme Plist **AirshipPreferenceCenterTheme.plist** ```xml viewController navigationBar title Preference Center titleFont fontName Helvetica fontSize 15 titleColor #0000FF preferenceCenter subtitleAppearance commonSection titleAppearance color #de0000 font fontName Helvetica fontSize 32 subtitleAppearance color #da833b font fontName Helvetica fontSize 25 labeledSectionBreak titleAppearance channelSubscription titleAppearance color #034710 font fontName Helvetica fontSize 20 subtitleAppearance color #8fe388 font fontName Helvetica fontSize 15 contactSubscription titleAppearance color #034710 font fontName Helvetica fontSize 20 subtitleAppearance color #8fe388 font fontName Helvetica fontSize 15 contactSubscriptionGroup titleAppearance color #034710 font fontName Helvetica fontSize 20 subtitleAppearance color #8fe388 font fontName Helvetica fontSize 15 chip checkColor #3bd2d6 borderColor #0a0fc9 labelAppearance color #7c6bea font fontName Helvetica fontSize 15 alert titleAppearance color #0a0fc9 font fontName Helvetica fontSize 15 subtitleAppearance color #d1b4d4 buttonLabelAppearance color #78c8c0 font fontName Helvetica fontSize 25 buttonBackgroundColor #da833b ``` # Embed the Preference Center > Embed the Preference Center view directly in your app's navigation instead of displaying it as an overlay. By default, Airship displays the Preference Center as an overlay on top of your app. For tighter integration with your app's navigation flow, you can embed the Preference Center views directly into your app. ## Embedding the Preference Center View The `PreferenceCenterView` (Swift) or `UAPreferenceCenterViewControllerFactory` (Objective-C) provides a complete Preference Center with a built-in navigation stack. #### Swift ```swift import SwiftUI import AirshipPreferenceCenter struct MyPreferenceCenterScreen: View { var body: some View { PreferenceCenterView(preferenceCenterID: "my_preference_center_id") } } ``` #### Objective-C ```objc @import AirshipCore; // Create a view controller UIViewController *preferenceCenterVC = [UAPreferenceCenterViewControllerFactory makeViewControllerWithPreferenceCenterID:@"my_preference_center_id"]; // Present or push the view controller [self.navigationController pushViewController:preferenceCenterVC animated:YES]; ``` Or embed it in a container view: ```objc @import AirshipCore; // Embed the preference center in a container view NSError *error = nil; UIView *containerView = [UAPreferenceCenterViewControllerFactory embedWithPreferenceCenterID:@"my_preference_center_id" preferenceCenterThemePlist:nil inParentViewController:self error:&error]; if (error) { NSLog(@"Failed to embed preference center: %@", error); } else { // Add the container view to your view hierarchy containerView.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:containerView]; [NSLayoutConstraint activateConstraints:@[ [containerView.topAnchor constraintEqualToAnchor:self.view.topAnchor], [containerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [containerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], [containerView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] ]]; } ``` ### Content View Without Navigation Stack For more control over the navigation, use `PreferenceCenterContent` (Swift only) which provides just the content without a built-in navigation stack. ```swift import SwiftUI import AirshipPreferenceCenter struct MyPreferenceCenterScreen: View { var body: some View { NavigationView { PreferenceCenterContent(preferenceCenterID: "my_preference_center_id") .navigationTitle("Preferences") } } } ``` > **Note:** To apply a custom theme globally, see [Getting Started: Applying a Custom Theme](https://www.airship.com/docs/developer/sdk-integration/apple/preference-center/getting-started/#applying-a-custom-theme). ## Customizing View Styles (Swift Only) For SwiftUI views, you can apply style overrides to customize individual components within the Preference Center. These view modifiers allow granular control over specific sections without affecting the global theme. ### Available Style Overrides - **`.channelSubscriptionStyle(_:)`** - Customize channel subscription views - **`.commonSectionViewStyle(_:)`** - Adjust common sections - **`.contactManagementSectionStyle(_:)`** - Modify contact management sections - **`.contactSubscriptionGroupStyle(_:)`** - Tailor contact subscription groups - **`.contactSubscriptionStyle(_:)`** - Customize individual contact subscriptions - **`.labeledSectionBreakStyle(_:)`** - Define labeled section breaks - **`.alertStyle(_:)`** - Adjust alert appearance ### Example ```swift import SwiftUI import AirshipPreferenceCenter struct MyPreferenceCenterScreen: View { var body: some View { PreferenceCenterView(preferenceCenterID: "my_preference_center_id") .channelSubscriptionStyle(MyCustomChannelStyle()) .contactSubscriptionStyle(MyCustomContactStyle()) .alertStyle(MyCustomAlertStyle()) } } // Define custom styles by conforming to the respective protocols struct MyCustomChannelStyle: ChannelSubscriptionStyle { // Implement required style methods } struct MyCustomContactStyle: ContactSubscriptionStyle { // Implement required style methods } struct MyCustomAlertStyle: AlertStyle { // Implement required style methods } ``` For detailed information on creating custom styles and the available properties for each style protocol, see the [AirshipPreferenceCenter View extension documentation](https://urbanairship.github.io/ios-library/v20/AirshipPreferenceCenter/documentation/airshippreferencecenter/swiftuicore/view). ## Advanced: Custom Loading (Swift Only) For SwiftUI apps, you can customize the loading behavior and respond to phase changes using `PreferenceCenterContent`: ```swift import SwiftUI import AirshipPreferenceCenter struct MyPreferenceCenterScreen: View { var body: some View { NavigationView { PreferenceCenterContent( preferenceCenterID: "my_preference_center_id", onLoad: { preferenceCenterID in // Custom loading logic // Return a PreferenceCenterContentPhase return await loadPreferenceCenter(preferenceCenterID) }, onPhaseChange: { phase in switch phase { case .loading: print("Loading preference center...") case .error(let error): print("Failed to load: \(error)") case .loaded(let state): print("Loaded with state: \(state)") } } ) .navigationTitle("Preferences") } } func loadPreferenceCenter(_ id: String) async -> PreferenceCenterContentPhase { // Custom loading implementation // Return .loading, .error, or .loaded return .loading } } ``` The `PreferenceCenterViewPhase` enum represents the current state of the Preference Center: - `.loading` — The view is loading - `.error(Error)` — The view failed to load the config - `.loaded(PreferenceCenterState)` — The view is loaded with the state ## Handling Display Requests When embedding a Preference Center, you need to intercept Airship's display requests and navigate to your embedded view instead of showing the default overlay. #### Swift ```swift Airship.preferenceCenter.onDisplay = { preferenceCenterID in guard preferenceCenterID == "my_embedded_preference_center_id" else { // Not the embedded one, allow Airship to display it as an overlay return false } // Navigate to your embedded view // Example: Use your app's navigation system to show the screen NotificationCenter.default.post( name: .showEmbeddedPreferenceCenter, object: nil, userInfo: ["id": preferenceCenterID] ) // Return true to indicate you handled the display return true } ``` #### Objective-C ```objc @import AirshipCore; // In your app delegate or preference center manager @interface MyAppDelegate () @end @implementation MyAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Set the open delegate UAirship.preferenceCenter.openDelegate = self; return YES; } - (BOOL)openPreferenceCenter:(NSString *)preferenceCenterID { if (![preferenceCenterID isEqualToString:@"my_embedded_preference_center_id"]) { // Not the embedded one, allow Airship to display it as an overlay return NO; } // Navigate to your embedded view [[NSNotificationCenter defaultCenter] postNotificationName:@"ShowEmbeddedPreferenceCenter" object:nil userInfo:@{@"id": preferenceCenterID}]; // Return YES to indicate you handled the display return YES; } @end ``` By returning `true` (or `YES` in Objective-C), you tell Airship that you've handled the display request, preventing the default overlay from appearing. ### Audience Management Integrate audience management features into your app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs, listen for channel creation, and configure the channel capture tool. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. If `AirshipMessageCenter` is installed, the SDK will attempt to restore the Channel ID across app reinstalls. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. #### Swift ```swift let channelID = Airship.channel.identifier ``` #### Objective-C ```objc NSString *channelID = UAirship.channel.identifier; ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. #### Swift Using `identifierUpdates` (AsyncStream): ```swift Task { for await channelID in Airship.channel.identifierUpdates { print("Channel ID: \(channelID)") } } ``` Using `NotificationCenter`: ```swift NotificationCenter.default.addObserver( self, selector: #selector(refreshView), name: AirshipNotifications.ChannelCreated.name, object: nil ) ``` #### Objective-C Using `NotificationCenter`: ```objc [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshView) name:UAirshipNotificationChannelCreated.name object:nil]; ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring `AirshipConfig`, see [iOS SDK Setup](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/). #### Swift ```swift config.isChannelCaptureEnabled = false ``` #### Objective-C ```objc config.isChannelCaptureEnabled = NO; ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during takeOff. For more information about Privacy Manager, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/apple/data-collection/privacy-manager/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. #### Swift ```swift Airship.contact.identify("some named user ID") ``` #### Objective-C ```objective-c [UAirship.contact identify:@"some named user ID"]; ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. #### Swift ```swift Airship.contact.reset() ``` #### Objective-C ```objc [UAirship.contact reset]; ``` You can get the Named User ID only if you set it through the SDK. #### Swift ```swift await Airship.contact.namedUserID ``` #### Objective-C ```objc [UAirship.contact getNamedUserIDWithCompletionHandler:^(NSString *namedUserID) { }]; ``` ### Email channel association When an email address is registered through the SDK, it will be registered for both transactional and commercial emails by default. To change this behavior, you can override the options to request [[Double Opt-In](https://www.airship.com/docs/reference/glossary/#double_opt_in)](https://www.airship.com/docs/developer/api-integrations/email/getting-started/#double-opt-in) for commercial messages. #### Swift ```swift let options = EmailRegistrationOptions.commercialOptions( transactionalOptedIn: transactionalDate, commercialOptedIn: commercialDate, properties: properties ) Airship.contact.registerEmail("your@example.com", options: options) ``` #### Objective-C ```objc UAEmailRegistrationOptions* options = [UAEmailRegistrationOptions commercialOptionsWithTransactionalOptedIn:transactionalDate commercialOptedIn:commercialDate properties:properties]; [UAirship.contact registerEmail:@"your@example.com" options:options]; ``` ### SMS channel association When an [MSISDN](https://www.airship.com/docs/reference/glossary/#msisdn) is registered through the SDK, Airship sends a message to that number, prompting them to opt in. For more information, see the SMS platform documentation: [Non-Mobile Double Opt-In](https://www.airship.com/docs/developer/api-integrations/sms/opt-in-out-handling/#non-mobile-double-opt-in). #### Swift ```swift let options = SMSRegistrationOptions.optIn(senderID: "senderId") Airship.contact.registerSMS("yourMsisdn", options: options) ``` #### Objective-C ```objc UASMSRegistrationOptions* options = [UASMSRegistrationOptions optInSenderID:@"senderId"]; [UAirship.contact registerSMS:"yourMsisdn" options:options]; ``` ### Open Channel association Open Channels support notifications to any medium that can accept a JSON payload, through either the Airship API or web dashboard. For more information about Open Channels, see the [Open Channels documentation](https://www.airship.com/docs/developer/api-integrations/open/getting-started/). #### Swift ```swift let options = OpenRegistrationOptions.optIn(platformName: "platformName", identifiers: identifiers) Airship.contact.registerOpen("address", options: options) ``` #### Objective-C ```objc UAOpenRegistrationOptions* options = [UAOpenRegistrationOptions optInSenderID:@"platformName"]; [UAirship.contact registerOpen:"address" options:options]; ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. #### Swift ```swift Airship.channel.editTags { editor in editor.set(["one", "two", "three"]) editor.add("a_tag") editor.remove("three") } // Accessing channel tags let tags = Airship.channel.tags ``` #### Objective-C ```objc [UAirship.channel editTags:^(UATagEditor *editor) { [editor setTags:@[@"one", @"two", @"three"]]; [editor addTag:@"a_tag"]; [editor removeTag:@"three"]; }]; // Accessing channel tags NSArray* tags = [[UAirship channel] tags]; ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. #### Swift ```swift Airship.channel.editTagGroups { editor in editor.add(["silver-member", "gold-member"], group:"loyalty") editor.remove(["bronze-member", "club-member"], group:"loyalty") editor.set(["bingo"], group:"games") } ``` #### Objective-C ```objc [UAirship.channel editTagGroups:^(UATagGroupsEditor *editor) { [editor addTags:@[@"silver-member", @"gold-member"] group:@"loyalty"]; [editor removeTags:@[@"bronze-member", @"club-member"] group:@"loyalty"]; [editor setTags:@[@"bingo"] group:@"games"]; }]; ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. #### Swift ```swift Airship.contact.editTagGroups { editor in editor.add(["silver-member", "gold-member"], group:"loyalty") editor.remove(["bronze-member", "club-member"], group:"loyalty") editor.set(["bingo"], group:"games") } ``` #### Objective-C ```objc [UAirship.contact editTagGroups:^(UATagGroupsEditor *editor) { [editor addTags:@[@"silver-member", @"gold-member"] group:@"loyalty"]; [editor removeTags:@[@"bronze-member", @"club-member"] group:@"loyalty"]; [editor setTags:@[@"bingo"] group:@"games"]; }]; ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. #### Swift ```swift Airship.channel.editAttributes { editor in editor.set(string: "Bobby's Phone", attribute: "device_name") editor.set(number: 4.99, attribute: "average_rating") editor.remove("vip_status") } ``` #### Objective-C ```objc [UAirship.channel editAttributes:^(UAAttributesEditor * editor) { [editor setString:@"Bobby's Phone" attribute:@"device_name"]; [editor setNumber:@(4.99) attribute:@"average_rating"]; [editor removeAttribute:@"vip_status"]; }]; ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. #### Swift ```swift Airship.contact.editAttributes { editor in editor.set(string: "Bobby", attribute: "first_name") } ``` #### Objective-C ```objc [UAirship.contact editAttributes:^(UAAttributesEditor * editor) { [editor setString:@"Bobby" attribute:@"first_name"]; }]; ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. You can set and remove JSON Attributes on a Channel or a Contact. #### Swift ```swift Airship.contact.editAttributes { editor in try! editor.set( json: [ "key": .string("value"), "another_key": .string("another_value") ], attribute: "attribute_name", instanceID: "instance_id", expiration: Date.now ) } ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. #### Swift ```swift // Modifying channel subscription lists Airship.channel.editSubscriptionLists { editor in editor.subscribe("food") editor.unsubscribe("sports") } // Fetching channel subscription lists let channelSubscriptions = try await Airship.channel.fetchSubscriptionLists() ``` #### Objective-C ```objc // Modifying channel subscription lists UASubscriptionListEditor *channelEditor = [UAirship.channel editSubscriptionLists]; [channelEditor subscribe:@"food"]; [channelEditor unsubscribe:@"sports"]; [channelEditor apply]; // Fetching channel subscription lists [[UAChannel shared] fetchSubscriptionListsWithCompletionHandler:^(NSArray * _Nullable channelSubscriptionLists, NSError * _Nullable error) { // Use the channelSubscriptionLists }]; ``` ## Contact Subscription Lists Contact subscriptions are set at the user-level and require a Channel scope specifying the types that the subscription list applies to. #### Swift ```swift // Modifying contact subscription lists Airship.contact.editSubscriptionLists { editor in editor.subscribe("food", scope: .app) editor.unsubscribe("sports", scope: .sms) } // Fetching contact subscription lists let contactSubscriptions = try await Airship.contact.fetchSubscriptionLists() ``` #### Objective-C ```objc // Modifying contact subscription lists UAScopedSubscriptionListEditor *contactEditor = [[UAContact shared] editSubscriptionLists]; [contactEditor subscribe:"food" scope:"app"]; [contactEditor unsubscribe:"sports" scope:"sms"]; [contactEditor apply]; // Fetching contact subscription lists [UAirship.contact fetchSubscriptionListsWithCompletionHandler:^(NSDictionary * _Nullable, NSError * _Nullable) { // Use the subscriptionLists }]; ``` ## Verifying Subscription Lists To verify that subscription lists have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ### Troubleshooting Common issues and solutions for Airship SDK setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) or [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/apple/installation/advanced-integration/), if you don't see a channel ID in the console logs or encounter errors during initialization, review the following common problems and solutions. ## takeOff errors The `takeOff` method throws an error in these cases: - `takeOff` has already been successfully called. - `takeOff` was called without an `AirshipConfig` instance and the SDK could not load or parse `AirshipConfig.plist`. - `takeOff` was called without an `AirshipConfig` instance and the parsed `AirshipConfig.plist` is invalid due to missing credentials. - `takeOff` was called with an `AirshipConfig` instance that has an invalid config due to missing credentials. ## takeOff called multiple times If `takeOff` throws because it has already been successfully called, verify the following: - `takeOff` is only called once per app launch - It's not called in both `application(_:didFinishLaunchingWithOptions:)` and your App's `init()` method - For SwiftUI apps, `takeOff` is called only in the App's `init()` method ## takeOff credentials The `takeOff` method only validates that credentials are present and formatted correctly. It does not verify that the credentials are valid against Airship servers. If the config is properly set up and Airship is only called once, no error will be thrown even if the credentials themselves are invalid. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. **Symptoms of missing or invalid credentials:** - No channel ID appears in the console logs - Warnings or errors in the logs after initialization - Channel is not created in the Airship dashboard If you are experiencing credential issues, do the following: 1. Compare your Airship project credentials with the values in your app, either in code or in `AirshipConfig.plist`. - Credentials must match the expected format and character set. - Credentials must not be empty strings or contain extra whitespace. 1. Ensure both `productionAppKey`/`productionAppSecret` and `developmentAppKey`/`developmentAppSecret` are set before calling `takeOff`. **Guidelines for credentials:** - Use development credentials for development builds and production credentials for release and TestFlight builds. - Configure both development and production credentials in your app, either in code or in `AirshipConfig.plist`. The SDK chooses which to use based on your build configuration. ## AirshipConfig.plist not found or invalid If `takeOff` fails because the SDK could not load or parse `AirshipConfig.plist`, or the plist is invalid, verify the following: - The file exists in your app bundle - The file is included in your target's Copy Bundle Resources build phase - All required keys are present in the plist file: `productionAppKey`, `productionAppSecret`, `developmentAppKey`, and `developmentAppSecret` - The plist file format is valid XML # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. The SDK provides detailed information about their current state. ## Get Current Notification Status Read the current notification status from `Airship.push.notificationStatus` to inspect each field: #### Swift ```swift let status = await Airship.push.notificationStatus print("User notifications enabled: \(status.isUserNotificationsEnabled)") print("Notifications allowed: \(status.areNotificationsAllowed)") print("Privacy feature enabled: \(status.isPushPrivacyFeatureEnabled)") print("Push token registered: \(status.isPushTokenRegistered)") print("User opted in: \(status.isUserOptedIn)") print("Fully opted in: \(status.isOptedIn)") print("Display status: \(status.displayNotificationStatus)") ``` #### Objective-C > **Note:** This async property is not available in Objective-C. Use the `userPushNotificationsEnabled` property and check authorization status directly with `UNUserNotificationCenter`. ## Listen for Status Changes Use the following to monitor notification status changes in real time: #### Swift ```swift Task { for await status in await Airship.push.notificationStatusUpdates { print("Notification status changed:") print("User opted in: \(status.isUserOptedIn)") print("Fully opted in: \(status.isOptedIn)") } } ``` #### Objective-C > **Note:** This async stream is not available in Objective-C. Use the `userPushNotificationsEnabled` property and check authorization status directly with `UNUserNotificationCenter`. ## Understanding Notification Status Fields The `AirshipNotificationStatus` struct provides detailed information about why push might not be working: | Field | Description | |-------|-------------| | `isUserNotificationsEnabled` | Whether `Airship.push.userPushNotificationsEnabled` is set to `true` | | `areNotificationsAllowed` | Whether the user has granted notification permissions (at least one authorized type) | | `isPushPrivacyFeatureEnabled` | Whether the push privacy feature is enabled in `AirshipPrivacyManager` | | `isPushTokenRegistered` | Whether a push token has been successfully registered with the system | | `displayNotificationStatus` | The system permission status (`.granted`, `.denied`, `.notDetermined`, `.ephemeral`) | | `isUserOptedIn` | `true` if user notifications are enabled, privacy feature is enabled, notifications are allowed, and display status is granted | | `isOptedIn` | `true` if `isUserOptedIn` is `true` AND a push token is registered | ## Common Status Scenarios **Status:** `isUserNotificationsEnabled = false` - **Cause:** `Airship.push.userPushNotificationsEnabled` has not been set to `true`. - **Solution:** Enable user notifications in your app code. **Status:** `areNotificationsAllowed = false` - **Cause:** User denied notification permissions or permissions not yet requested. - **Solution:** Request notification permissions or guide user to system settings. **Status:** `isPushPrivacyFeatureEnabled = false` - **Cause:** Push privacy feature is disabled in Privacy Manager. - **Solution:** Enable the push privacy feature: `Airship.privacyManager.enabledFeatures = [.push]`. **Status:** `isPushTokenRegistered = false` - **Cause:** Device hasn't received a push token from APNs yet. - **Solution:** Check network connectivity, APNs certificate configuration, and device/simulator limitations. **Status:** `isUserOptedIn = true` but `isOptedIn = false` - **Cause:** Push token registration is pending or failed. - **Solution:** Check console logs for APNs registration errors, verify network connectivity, and ensure proper entitlements. # Troubleshooting Notification Service Extensions > Verify notification service extension setup and debug issues. If the basic verification steps do not resolve your [notification service extension](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/) (NSE) issue, use the advanced debugging steps to isolate the cause. ## Basic verification First, perform basic verifications: 1. Verify the deployment target includes the device you are testing it against. Airship supports 16.0+. ![Deployment target setting in Xcode](https://www.airship.com/docs/images/nse-troubleshoot-deployment-version_hu_5e5f95dfb9a1775e.webp) *Deployment target setting in Xcode* 1. Verify the extension target links the `AirshipNotificationServiceExtension` framework and none of the other Airship frameworks. ![Extension target linked to AirshipNotificationServiceExtension only](https://www.airship.com/docs/images/extension-target-links-airship-notification-service-extension_hu_a3504fa3a2c481b6.webp) *Extension target linked to AirshipNotificationServiceExtension only* 1. Verify the main app target links to your extension. ![Main app target embedding the extension](https://www.airship.com/docs/images/main-app-target-extension_hu_fed870069ef4ef29.webp) *Main app target embedding the extension* 1. Verify the extension bundle ID follows the app bundle ID with a suffix. For example, `com.example.app.NotificationServiceExtension`. ## Advanced debugging If issues persist, isolate the problem by building up from Apple's default implementation: 1. Replace your NSE implementation with Apple's default NSE that only modifies the notification title. If that fails, recreate the extension and repeat the basic verification steps. #### Swift ```swift import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { // Modify the notification content here... bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" contentHandler(bestAttemptContent) } } override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } } ``` 1. Confirm the extension can modify the title when using Apple's default implementation. 1. Link the Airship NSE framework to your extension and have your NSE extend the Airship NSE class instead of `UNNotificationServiceExtension`. Keep your existing title-modification logic for now. #### Swift ```swift import UserNotifications import AirshipNotificationServiceExtension class NotificationService: UANotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { // Modify the notification content here... bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" contentHandler(bestAttemptContent) } } override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } } ``` 1. Confirm you still receive the modified title. This verifies the Airship NSE framework is linked correctly. 1. Remove the title-modification test code and implement the full Airship NSE integration. #### Swift ```swift import UserNotifications import AirshipNotificationServiceExtension class NotificationService: UANotificationServiceExtension { } ``` 1. Test that rich media, such as images, displays correctly. 1. Add verbose logging to troubleshoot the issue if none of these steps resolved the issue. #### Swift ```swift import UserNotifications import AirshipNotificationServiceExtension class NotificationService: UANotificationServiceExtension { override var airshipConfig: AirshipExtensionConfig { return AirshipExtensionConfig( logLevel: .verbose, logHandler: .publicLogger ) } } ``` ### Data Collection Overview of data collection and controls provided by the Airship SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: #### Swift | Swift Constant | Features | Config Value | |----------------|----------|--------------| | `AirshipFeature.push` | Push notifications | `push` | | `AirshipFeature.inAppAutomation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | `in_app_automation` | | `AirshipFeature.messageCenter` | Message Center | `message_center` | | `AirshipFeature.tagsAndAttributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | `tags_and_attributes` | | `AirshipFeature.contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | `contacts` | | `AirshipFeature.analytics` | Associated identifiers, Custom events, Screen tracking, Surveys (questions and NPS surveys in [Scenes](https://www.airship.com/docs/reference/glossary/#scene)), email address (via form inputs in Scenes), [Feature Flag](https://www.airship.com/docs/reference/glossary/#feature_flag) interaction | `analytics` | | `AirshipFeature.featureFlags` | Feature Flag evaluation and interaction | `feature_flags` | | `AirshipFeature.all` | All features | `all` | | `[]` | No features | `none` | #### Objective-C | Objective-C Constant | Features | Config Value | |----------------------|----------|--------------| | `UAFeatures.push` | Push notifications | `push` | | `UAFeatures.inAppAutomation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | `in_app_automation` | | `UAFeatures.messageCenter` | Message Center | `message_center` | | `UAFeatures.tagsAndAttributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | `tags_and_attributes` | | `UAFeatures.contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | `contacts` | | `UAFeatures.analytics` | Associated identifiers, Custom events, Screen tracking, Surveys (questions and NPS surveys in [Scenes](https://www.airship.com/docs/reference/glossary/#scene)), email address (via form inputs in Scenes), [Feature Flag](https://www.airship.com/docs/reference/glossary/#feature_flag) interaction | `analytics` | | `UAFeatures.featureFlags` | Feature Flag evaluation and interaction | `feature_flags` | | `UAFeatures.all` | All features | `all` | | `UAFeatures.none` | No features | `none` | ## Configuring default enabled features You can configure which features are enabled by default when the SDK initializes. This is done in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff). ### Default configuration (all features enabled) By default, all features are enabled. The SDK will collect data and make network requests as configured. ### Disabling all features To disable all features by default (useful for consent opt-in flows), set the enabled features to an empty array: #### Swift **Config** ```swift let config = AirshipConfig() config.enabledFeatures = [] try! Airship.takeOff(config) ``` **AirshipConfig.plist** ```xml ... enabledFeatures ... ``` #### Objective-C **UAConfig** ```objc UAConfig *config = [UAConfig config]; config.enabledFeatures = UAFeaturesNone; [UAirship takeOff:config error:&airshipError]; ``` **AirshipConfig.plist** ```xml ... enabledFeatures ... ``` ### Enabling specific features To enable only specific features by default: #### Swift **Config** ```swift let config = AirshipConfig() config.enabledFeatures = [.push, .analytics] try! Airship.takeOff(config) ``` **AirshipConfig.plist** ```xml ... enabledFeatures push analytics ... ``` #### Objective-C **UAConfig** ```objc UAConfig *config = [UAConfig config]; config.enabledFeatures = UAFeaturesPush | UAFeaturesAnalytics; [UAirship takeOff:config error:&airshipError]; ``` **AirshipConfig.plist** ```xml ... enabledFeatures push analytics ... ``` ### Resetting features on each app start If you need to gather consent on each app start, enable `resetEnabledFeatures` to reset enabled features to the config defaults on each `takeOff`, ignoring any previously persisted settings: #### Swift ```swift var config = AirshipConfig() config.enabledFeatures = [] config.resetEnabledFeatures = true try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; config.enabledFeatures = UAFeaturesNone; config.resetEnabledFeatures = YES; [UAirship takeOff:config error:&airshipError]; ``` ## Modifying enabled features at runtime You can enable or disable features at any time after takeOff. Once you modify the enabled features from the default, those settings are persisted between app launches (unless `resetEnabledFeatures` is enabled in your config). ### Enabling features #### Swift ```swift Airship.privacyManager.enableFeatures([.push, .analytics]) ``` #### Objective-C ```objc UAFeature *features = [[UAFeature alloc] initFrom:@[UAFeature.push, UAFeature.analytics]]; [UAirship.privacyManager enableFeatures:features]; ``` ### Disabling features #### Swift ```swift Airship.privacyManager.disableFeatures([.push, .analytics]) ``` #### Objective-C ```objc UAFeature *features = [[UAFeature alloc] initFrom:@[UAFeature.push, UAFeature.analytics]]; [UAirship.privacyManager disableFeatures:features]; ``` ## Consent opt-in flow example A common use case is to start with all features disabled, then enable them as users grant consent: #### Swift ```swift // Start with all features disabled var config = AirshipConfig() config.enabledFeatures = [] try! Airship.takeOff(config) // Later, when user grants consent: func userGrantedConsent() { // Enable features based on user's consent choices Airship.privacyManager.enableFeatures([.push, .analytics]) } ``` #### Objective-C ```objc // Start with all features disabled UAConfig *config = [UAConfig config]; config.enabledFeatures = UAFeaturesNone; [UAirship takeOff:config error:&airshipError]; // Later, when user grants consent: - (void)userGrantedConsent { // Enable features based on user's consent choices UAFeature *features = [[UAFeature alloc] initFrom:@[UAFeature.push, UAFeature.analytics]]; [UAirship.privacyManager enableFeatures:features]; } ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Apple Privacy Manifest](https://www.airship.com/docs/reference/data-collection/apple-privacy-manifest/) - Declare data collection practices to Apple - [Analytics](https://www.airship.com/docs/developer/sdk-integration/apple/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers - [Permission Gathering](https://www.airship.com/docs/developer/sdk-integration/apple/data-collection/permission-gathering/) - Request additional permissions from users # Permission Prompts > Request additional system permissions (e.g., location) from users using Opt-in Actions.The Airship SDK automatically handles push notification permissions. For additional permissions like location, you can use Opt-in Actions to prompt users using native permission prompts. Opt-in Actions are a special type of [Action](https://www.airship.com/docs/reference/glossary/#action) that are handled by `PermissionsManager`. For an overview of all supported actions and where they are available, see the [Actions](https://www.airship.com/docs/guides/messaging/messages/actions/) guide. ## Supported Opt-in Types * Push — Handled automatically by the SDK (no implementation needed) * Location — Requires implementing a custom `PermissionDelegate` ## Implementing Location Opt-in To implement Location Opt-in, create a custom `PermissionDelegate` and register it with `PermissionsManager` to handle location permissions. ### Create a Location Permission Delegate #### Swift ```swift import Foundation import CoreLocation import AirshipCore import Combine class LocationPermissionDelegate: AirshipPermissionDelegate { let locationManager = CLLocationManager() @MainActor func checkPermissionStatus() async -> AirshipCore.AirshipPermissionStatus { return self.status } @MainActor func requestPermission() async -> AirshipCore.AirshipPermissionStatus { guard (self.status == .notDetermined) else { return self.status } guard (AppStateTracker.shared.state == .active) else { return .notDetermined } locationManager.requestAlwaysAuthorization() await waitActive() return self.status } var status: AirshipPermissionStatus { switch(locationManager.authorizationStatus) { case .notDetermined: return .notDetermined case .restricted: return .denied case .denied: return .denied case .authorizedAlways: return .granted case .authorizedWhenInUse: return .granted @unknown default: return .notDetermined } } } @MainActor private func waitActive() async { var subscription: AnyCancellable? await withCheckedContinuation { continuation in subscription = NotificationCenter.default.publisher(for: AppStateTracker.didBecomeActiveNotification) .first() .sink { _ in continuation.resume() } } subscription?.cancel() } ``` #### Objective-C ```objc #import #import @interface LocationPermissionDelegate : NSObject @property (nonatomic, strong) CLLocationManager *locationManager; @end @implementation LocationPermissionDelegate - (instancetype)init { self = [super init]; if (self) { _locationManager = [[CLLocationManager alloc] init]; } return self; } - (UAPermissionStatus)checkPermissionStatus { return [self status]; } - (void)requestPermissionWithCompletionHandler:(void (^)(UAPermissionStatus))completionHandler { if ([self status] != UAPermissionStatusNotDetermined) { completionHandler([self status]); return; } if ([UAAppStateTracker shared].state != UAAppStateActive) { completionHandler(UAPermissionStatusNotDetermined); return; } [self.locationManager requestAlwaysAuthorization]; // Wait for app to become active and check status dispatch_async(dispatch_get_main_queue(), ^{ completionHandler([self status]); }); } - (UAPermissionStatus)status { switch (self.locationManager.authorizationStatus) { case kCLAuthorizationStatusNotDetermined: return UAPermissionStatusNotDetermined; case kCLAuthorizationStatusRestricted: case kCLAuthorizationStatusDenied: return UAPermissionStatusDenied; case kCLAuthorizationStatusAuthorizedAlways: case kCLAuthorizationStatusAuthorizedWhenInUse: return UAPermissionStatusGranted; default: return UAPermissionStatusNotDetermined; } } @end ``` ### Register the Permission Delegate After creating a location `PermissionDelegate`, register it with `PermissionsManager` [after takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff): #### Swift ```swift Airship.permissionsManager.setDelegate( LocationPermissionDelegate(), permission: .location ) ``` #### Objective-C ```objc LocationPermissionDelegate *delegate = [[LocationPermissionDelegate alloc] init]; [[UAirship permissionsManager] setDelegate:delegate permission:UAPermissionLocation]; ``` ## Capacitor Integrate the Airship SDK into your Capacitor applications for iOS and Android. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Message Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/capacitor/message-center/getting-started/) and [Preference Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/capacitor/preference-center/getting-started/). ```js Airship.onDeepLink(event => { var deepLink = event.deepLink; // Handle deep link }); ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Running Actions You can run actions programmatically using the `Airship.actions.run()` method. The method returns a Promise that resolves with the action result. The action value can be a string, number, boolean, null, object, or array. **Running an action** ```typescript // Run an action with a string value using async/await try { const result = await Airship.actions.run("action_name", "action_value"); console.log("Action result:", result); } catch (error) { console.error("Action error:", error); } // Run an action with an object value try { const result = await Airship.actions.run("action_name", { key: "value", number: 42 }); console.log("Action result:", result); } catch (error) { console.error("Action error:", error); } // Run an action without a value try { const result = await Airship.actions.run("action_name"); console.log("Action result:", result); } catch (error) { console.error("Action error:", error); } // Run an action using Promise.then() Airship.actions.run("action_name", "action_value") .then((result) => { console.log("Action result:", result); }) .catch((error) => { console.error("Action error:", error); }); ``` ## Custom Actions You can register custom actions with an [Airship extender](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/extending-airship/) and with native SDK APIs. See the native platform documentation for details: - [iOS Actions](https://www.airship.com/docs/developer/sdk-integration/apple/actions/) - [Android Actions](https://www.airship.com/docs/developer/sdk-integration/android/actions/) # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. ```js const flag = await Airship.featureFlagManager.flag("YOUR_FLAG_NAME") if (flag.isEligible) { // Do something with the flag } else { // Disable feature or use default behavior } ``` ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/capacitor/data-collection/privacy-manager/). ```js await Airship.featureFlagManager.trackInteraction(flag) ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. ```js try { const flag = await Airship.featureFlagManager.flag("another_rad_flag") } catch (error) { console.log("error: " + error) } ``` # Live Activities > Integrate Live Activities into your Capacitor app to display real-time updates on the iOS Lock Screen and Dynamic Island. {{< badge "axp" >}} For the push API method, see the [iOS Live Activities](https://www.airship.com/docs/guides/messaging/features/ios-live-activities/) messaging guide. See also the [iOS Live Activities](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ### App setup Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/extending-airship/), make a call to `LiveActivityManager.shared.setup` to configure any Live Activities for the app. Call `configurator.register` for each Live Activity type that your application defines and include a block on how to parse the name of the activity that you will use to track on Airship. This name will be used to send updates through APNS. ```swift import Foundation import AirshipKit import AirshipFrameworkProxy import ActivityKit // This class header is required to be automatically picked up by the Airship plugin: @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { public static func onAirshipReady() { if #available(iOS 16.1, *) { // Will throw if called more than once try? LiveActivityManager.shared.setup { configurator in // Call for each Live Activity type await configurator.register(forType: Activity.self) { attributes in // Track this property as the Airship name for updates attributes.gameID } } } // other setup } } ``` ### Starting Live Activities For any Live Activities configured, you can start a new one using the `start` method: ```typescript Airship.iOS.liveActivityManager.start({ attributesType: 'SportsActivityAttributes', content: { state: { status: 'Game Pending', }, relevanceScore: 0.0, }, attributes: { gameID: 'sports-game-123', }, }); ``` ### Updating Live Activities To update, use `update`, but you will need the activity ID. ```typescript const activities = await Airship.iOS.liveActivityManager.listAll(); const activity = activities.find( (activity) => activity.attributes.gameID === 'sports-game-123' ); if (activity) { Airship.iOS.liveActivityManager.update({ activityId: activity.id, content: { state: { status: "Game starting!" } relevanceScore: 0.0, }, }); } ``` ### Ending Live Activities To end is similar to `update`. Use `end` with the activity ID: ```typescript const activities = await Airship.iOS.liveActivityManager.listAll(); const activity = activities.find( (activity) => activity.attributes.gameID === 'sports-game-123' ); if (activity) { Airship.iOS.liveActivityManager.end({ activityId: activity.id, dismissalPolicy: { type: "default" } }); } ``` # Live Updates > Integrate Live Updates into your Capacitor app to update content in real-time without requiring an app update. {{< badge "axp" >}} For the push API method, see the [Android Live Updates](https://www.airship.com/docs/guides/messaging/features/android-live-updates/) messaging guide. See also the [Android Live Updates](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ### Creating a handler The Airship SDK supports two types of Live Update handlers: * `NotificationLiveUpdateHandler` — Displays a notification with a custom layout, with content updated by the Live Update. * `CustomLiveUpdateHandler` — Receives Live Update events and provides flexibility to display content using a custom implementation. This can be used to power home screen widgets, views embedded in the app, and more. Each handler type has two different interfaces that may be implemented, to support suspending or callback-based code: * `SuspendLiveUpdateNotificationHandler` * `CallbackLiveUpdateNotificationHandler` * `SuspendLiveUpdateCustomHandler` * `CallbackLiveUpdateCustomHandler` The following `SampleLiveUpdateHandler` reads content from the Live Update payload and displays scores for a sports game in a custom notification layout, using `RemoteViews`: ```kotlin class SampleLiveUpdateHandler : SuspendLiveUpdateNotificationHandler() { override suspend fun onUpdate( context: Context, event: LiveUpdateEvent, update: LiveUpdate ): LiveUpdateResult { // Read content_state fields from the Live Update payload val teamOneScore = update.content.opt("team_one_score").getInt(0).toString() val teamTwoScore = update.content.opt("team_two_score").getInt(0).toString() val statusUpdate = update.content.opt("status_update").optString() // Expanded notification layout val bigLayout = RemoteViews(context.packageName, R.layout.sports_big).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) setTextViewText(R.id.statusUpdate, statusUpdate) } // Collapsed notification layout val smallLayout = RemoteViews(context.packageName, R.layout.sports_small).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) } // Create the notification builder val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_EVENT) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(smallLayout) .setCustomBigContentView(bigLayout) // Return 'ok' with the notification builder. // The Airship SDK will handle posting the notification. // Returning LiveUpdateResult.cancel() will end the Live Update and dismiss the notification. return LiveUpdateResult.ok(builder) } companion object { private const val NOTIFICATION_CHANNEL_ID = "sports" } } ``` ### Registering a handler Handlers must be registered with `LiveUpdateManager` in order to receive Live Update events. This should be done *once* after `takeOff`. Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/extending-airship/), register the types. ```kotlin @Keep public final class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { LiveUpdateManager.shared().run { register(type = "notification", handler = SampleLiveUpdateHandler()) } } } ``` > **Note:** The `type` used above, `"notification"`, is used to map Live Update events to the corresponding handler in your app. > The value can be any string that is unique across all handlers registered by an app. This also allows a single handler to manage multiple Live Updates that each have a unique `name`. ### Starting Live Updates Live Updates can be started from within the app. ```typescript Airship.android.liveUpdateManager.start({ name: 'sports-game-123' type: 'notification', content: { team_one_score: 0, team_two_score: 0, status_update: 'Game started!' } }); ``` ### Updating Live Updates Live Updates can be updated from within the app. ```typescript Airship.android.liveUpdateManager.update({ name: 'sports-game-123' content: { team_one_score: 3, team_two_score: 0, status_update: 'Game started!' } }); ``` ### Ending Live Updates You can end a Live Update from within the app. ```typescript Airship.android.liveUpdateManager.end({ name: 'sports-game-123' content: { team_one_score: 9, team_two_score: 6, status_update: 'Game over!' } }); ``` ### Clearing all active Live Updates During development, it can be useful to reset Live Update tracking on app launch. This allows any Live Updates to be started fresh, even if they were already started during a previous launch. To end all currently active Live Updates, call the `clearAll()` method on `LiveUpdateManager`. ```typescript Airship.android.liveUpdateManager.clearAll(); ``` # Capacitor Plugin Changelog > The latest updates to the Airship Capacitor plugin. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 6.0.0 May 8, 2026 Major release that updates Capacitor to 8.0. ### Changes - Updated Capacitor to 8.0 - Android `minSdkVersion` increased from 23 to 24 - Android Gradle wrapper updated to 8.14.3 ## 5.5.1 May 6, 2026 Patch release that fixes `Package.swift` not being included in the published npm package. ### Changes - Fixed `Package.swift` missing from the published npm package ## 5.5.0 May 1, 2026 Minor release that updates the Android SDK to 20.7.0 and the iOS SDK to 20.7.0. ### Changes - Updated Android SDK to [20.7.0](https://github.com/urbanairship/android-library/releases/tag/20.7.0) - Updated iOS SDK to [20.7.0](https://github.com/urbanairship/ios-library/releases/tag/20.7.0) ## 5.4.0 April 1, 2026 Minor release that updates the Android SDK to 20.6.1 and the iOS SDK to 20.6.0 ### Changes - Updated Android SDK to [20.6.1](https://github.com/urbanairship/android-library/releases/tag/20.6.1) - Updated iOS SDK to [20.6.0](https://github.com/urbanairship/ios-library/releases/tag/20.6.0) ## 5.3.0 March 18, 2026 Minor release that updates the Android SDK to 20.5.0 and the iOS SDK to 20.5.0. ### Changes - Updated Android SDK to [20.5.0](https://github.com/urbanairship/android-library/releases/tag/20.5.0) - Updated iOS SDK to [20.5.0](https://github.com/urbanairship/ios-library/releases/tag/20.5.0) ## 5.2.0 March 4, 2026 Minor release that updates the Android SDK to 20.4.0 and the iOS SDK to 20.4.0. ### Changes - Updated Android SDK to [20.4.0](https://github.com/urbanairship/android-library/releases/tag/20.4.0) - Updated iOS SDK to [20.4.0](https://github.com/urbanairship/ios-library/releases/tag/20.4.0) - Fixed missing back button on iOS when displaying a message with Airship.messageCenter.showMessageView ## 5.1.0 February 20, 2026 Minor release that fixes Airship failing to take off on iOS due to a plugin loader compatibility issue and updates the iOS SDK to 20.3.1 and the Android SDK to 20.2.2. ### Changes - Updated iOS SDK to [20.3.1](https://github.com/urbanairship/ios-library/releases/tag/20.3.1) - Updated Android SDK to [20.2.2](https://github.com/urbanairship/android-library/releases/tag/20.2.2) - Fixed iOS plugin loader to use the updated `onLoad()` protocol method - iOS minimum deployment target increased from 15.0 to 16.0 - Android compileSdkVersion updated to 36 - Android Kotlin version updated to 2.2.20 - Android Gradle Plugin updated to 8.13.0 ## 5.0.0 January 23, 2026 Major release that updates the Android SDK to 20.0.6 and the iOS SDK to 20.0.3 ### Changes - Updated Android SDK to [20.0.6](https://github.com/urbanairship/android-library/releases/tag/20.0.6) - Updated iOS SDK to [20.0.3](https://github.com/urbanairship/ios-library/releases/tag/20.0.3) ## 4.6.1 November 15, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Updated Android SDK to [19.13.6](https://github.com/urbanairship/android-library/releases/tag/19.13.6) - Updated iOS SDK to [19.11.2](https://github.com/urbanairship/ios-library/releases/tag/19.11.2) ## 4.6.0 August 27, 2025 Patch release that updates the Android SDK to 19.11.0 and the iOS SDK to 19.8.3 ### Changes - Updated Android SDK to [19.11.0](https://github.com/urbanairship/android-library/releases/tag/19.11.0) - Updated iOS SDK to [19.8.3](https://github.com/urbanairship/ios-library/releases/tag/19.8.3) ## 4.5.0 July 31, 2025 Minor release that updates the Android SDK to 19.10.0 and the iOS SDK to 19.7.0 ### Changes - Updated Android SDK to [19.10.0](https://github.com/urbanairship/android-library/releases/tag/19.10.0) - Updated iOS SDK to [19.7.0](https://github.com/urbanairship/ios-library/releases/tag/19.7.0) [All Releases](https://github.com/urbanairship/capacitor-airship/releases) ## 4.4.0 June 27, 2025 Minor release that adds support for Android log privacy level configuration and updates the Android SDK to 19.9.1 and the iOS SDK to 19.6.1. ### Changes - Updated Android SDK to [19.9.1](https://github.com/urbanairship/android-library/releases/tag/19.9.1) - Updated iOS SDK to [19.6.1](https://github.com/urbanairship/ios-library/releases/tag/19.6.1) - Added Android `logPrivacyLevel` configuration support ## 4.3.1 May 9, 2025 Patch release that updates iOS SDK to 19.3.2 ### Changes - Updated iOS SDK to [19.3.2](https://github.com/urbanairship/ios-library/releases/tag/19.3.2) ## 4.3.0 May 2, 2025 Minor release that updates the Android SDK to 19.6.2 and the iOS SDK to 19.3.1. ### Changes - Updated Android SDK to [19.6.2](https://github.com/urbanairship/android-library/releases/tag/19.6.2) - Updated iOS SDK to [19.3.1](https://github.com/urbanairship/ios-library/releases/tag/19.3.1) - Added support for JSON attributes - Added new method `Airship.channel.waitForChannelId()` that waits for the channel ID to be created ## 4.2.0 March 27, 2025 Minor release that updates the Android SDK to 19.4.0 and the iOS SDK to 19.1.1 ### Changes - Updated Android SDK to [19.4.0](https://github.com/urbanairship/android-library/releases/tag/19.4.0) - Updated iOS SDK to [19.1.1](https://github.com/urbanairship/ios-library/releases/tag/19.1.1) ## 4.1.0 March 12, 2025 Major release that updates to the latest Airship SDKs and exposes the analytics session ID. ### Changes - Updated Android SDK to [19.3.0](https://github.com/urbanairship/android-library/releases/tag/19.3.0). - Updated iOS SDK to [19.1.0](https://github.com/urbanairship/ios-library/releases/tag/19.1.0). - Added `Airship.analytics.getSessionId()` method. ## 4.0.1 February 12, 2025 Patch release to fix the `MessageCenterUpdatedEvent` property `messageUnreadCount` on iOS. ### Changes - Fixed MessageCenterUpdatedEvent.messageUnreadCount on iOS. ## 4.0.0 February 11, 2025 Major release that updates the Android Airship SDK to 19.1.0 and iOS Airship SDK to 19.0.3 ### Changes - Updated Android SDK to [19.1.0](https://github.com/urbanairship/android-library/releases/tag/19.1.0) - Updated iOS SDK to [19.0.3](https://github.com/urbanairship/ios-library/releases/tag/19.0.3) - Updated Capacitor to 7.0.0 - Fixed SPM integration - iOS requires Xcode 16.2 and iOS 15+ - Android requires compileSdkVersion 35+ and minSdkVersion 23+ ## 3.1.0 December 7, 2024 Minor release that updates the Android Airship SDK to 18.5.0 and iOS Airship SDK to 18.13.0 ### Changes - Updated Android SDK to [18.5.0](https://github.com/urbanairship/android-library/releases/tag/18.5.0). - Updated iOS SDK to [18.13.0](https://github.com/urbanairship/ios-library/releases/tag/18.13.0). ## 3.0.1 November 27, 2024 Patch release that updates the iOS Airship SDK to 18.12.2 and Android Airship SDK to 18.4.2 ### Changes - Updated Android SDK to [18.4.2](https://github.com/urbanairship/android-library/releases/tag/18.4.2). - Updated iOS SDK to [18.12.2](https://github.com/urbanairship/ios-library/releases/tag/18.12.2). - Updated Android plugin to use the project compileSdk, minSdkVersion, and targetSdkVersion. The plugin still requires compileSdk to be set to 35+. ## 3.0.0 October 26, 2024 Major version that makes it easier to include Airship in a hybrid app. The only breaking change is when extending the AirshipPluginExtender protocol on java there is a new extendConfig(Contex, AirshipConfigOptions.Builder) method to implement. Most application will not be affected. ### Changes - Added new methods to the plugin extender to make hybrid app integrations easier - Updated Airship Android SDK to [18.3.3](https://github.com/urbanairship/android-library/releases/tag/18.3.3) - Fixed tracking live activities started from a push notification ## 2.4.0 October 16, 2024 Minor release that updates the native SDKs and fixes an issue with `Airship.messageCenter.getUnreadCount()` ### Changes - Updated Airship iOS SDK to [18.11.1](https://github.com/urbanairship/ios-library/releases/tag/18.11.1) - Fixed method binding for `Airship.messageCenter.getUnreadCount()` - Fixed MessageCenterPredicate not being applied to the OOTB Message Center UI on iOS ## 2.3.0 October 7, 2024 Minor release that updates to latest SDK versions and adds support for iOS Live Activities & Android Live Updates. ### Changes - Updated Airship Android SDK to [18.3.2](https://github.com/urbanairship/android-library/releases/tag/18.3.2) - Updated Airship iOS SDK to [18.10.0](https://github.com/urbanairship/ios-library/releases/tag/18.10.0) - Added new APIs to manage [iOS Live Activities](https://docs.airship.com/platform/mobile/ios-live-activities/) - Added new APIs to manage [Android Live Updates](https://docs.airship.com/platform/mobile/android-live-updates/) - Added a new [Plugin Extenders](http://localhost:1313/platform/mobile/setup/sdk/capacitor/#extending-airship) to modify the native Airship SDK after takeOff ## 2.2.0 September 25, 2024 Minor release that updates the iOS SDK to 18.9.2 and adds the method `Airship.messageCenter.showMessageCenter(messageId?: string)` that can be used to show the OOTB Message Center UI even if auto launching Message Center is disabled. This new functionality is useful if the application needs to route the user in the app before processing the display event while still being able to use the OOTB UI. ### Changes - Updated Airship iOS SDK to 18.9.2. - Added new `showMessageCenter(messageId?: string)` to `MessageCenter`. ## 2.1.0 September 16, 2024 Minor release that adds `notificationPermissionStatus` to the `PushNotificationStatus` object and a way to specify the fallback when requesting permissions and the permission is already denied. ### Changes - Updated Airship Android SDK to 18.3.0 - Updated Airship iOS SDK to 18.9.1 - Added `notificationPermissionStatus` to `PushNotificationStatus` - Added options to `enableUserNotifications` to specify the `PromptPermissionFallback` when enabling user notifications ## 2.0.1 July 16, 2024 Patch release that fixes a critical issue with iOS that prevents the Airship library from automatically initializing. Apps using 2.0.0 should update. ### Changes - Added missing files to the npm package for iOS ## 2.0.0 July 3, 2024 Major release to support Capacitor 6. ### Changes - Updated Airship Android SDK to 18.1.1 - Updated Airship iOS SDK to 18.5.0 - Added iOS logPrivacyLevel that can be set in the environments when calling takeOff ## 1.2.4 June 21, 2024 Patch release to fix a regression on iOS with In-App Automations, Scenes, and Surveys ignoring screen, version, and custom event triggers. Apps using those triggers that are on 1.2.3 should update. ### Changes - Updated iOS SDK to 18.4.1 - Fixed regression with triggers ## 1.2.3 June 20, 2024 Patch release that updates to latest iOS SDK. ### Changes - Updated iOS SDK to 18.4.0 ## 1.2.2 May 17, 2024 Patch release that updates to latest iOS SDK. ### Changes - Updated iOS SDK to 18.2.2 [View Older Releases](https://github.com/urbanairship/capacitor-airship/releases?q=created%3A%3C2024-05-15&expanded=true) # Capacitor Plugin Resources > API documentation, source code, and changelogs for the Airship Capacitor plugin. ## Platform Support {#platform-support} | Feature | iOS | Android | |----------------------------------------|-----|---------| | Push Notifications | ✅ | ✅ | | Live Activities | ✅ | ❌ | | Live Updates | ❌ | ✅ | | In-App Experiences | ✅ | ✅ | | Custom Views | ✅ | ✅ | | Embedded Content | ❌ | ❌ | | Message Center | ✅ | ✅ | | Preference Center | ✅ | ✅ | | Feature Flags | ✅ | ✅ | | Analytics | ✅ | ✅ | | Contacts | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ## API References * [Capacitor API Documentation](https://www.airship.com/docs/reference/libraries/capacitor-airship/latest/) ## Source * [Source](https://github.com/urbanairship/capacitor-airship) ## Changelog * [Capacitor Changelog](https://www.airship.com/docs/developer/sdk-integration/capacitor/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [Capacitor License](https://github.com/urbanairship/capacitor-airship/blob/main/LICENSE) ### SDK Installation Complete installation and configuration guides for the Airship Capacitor plugin. # Install and Set Up the Capacitor Plugin > How to install the Airship Capacitor plugin.## Requirements - Capacitor 5.0.0 or higher - iOS 15+ (Xcode 16+) - Android API 23+ ## Setup Install the plugin ```bash npm install @ua/capacitor-airship npx cap sync ``` ## Initialize Airship Before you can access any of the module's API, the Airship module needs to be initialized. This can be accomplished by calling `takeOff` when the device is ready with the proper config or by providing plugin config in the `capacitor.config.json` file. **Calling takeOff** ```typescript // TakeOff await Airship.takeOff({ production: { appKey: "", appSecret: "" }, development: { appKey: "", appSecret: "" }, inProduction: true, site: "us", // use "eu" for EU cloud projects urlAllowList: ["*"], android: { notificationConfig: { icon: "ic_notification", accentColor: "#00ff00" } } }) ``` **Using capacitor.config.json** ```json { "appId":"com.example.plugin", "appName":"example", "bundledWebRuntime":false, "webDir":"dist", "plugins":{ "SplashScreen":{ "launchShowDuration":0 }, "Airship":{ "config":{ "default":{ "appKey":"", "appSecret":"" }, "site":"us", "urlAllowList":[ "*" ], "android":{ "notificationConfig":{ "icon":"ic_notification", "accentColor":"#00ff00" } } } } }, "server":{ "androidScheme":"https" } } } ``` Takeoff can be called multiple times, but the config passed in `takeOff` won't be applied until the next app init. For a complete list of configuration options, see the [AirshipConfig reference](https://www.airship.com/docs/reference/libraries/capacitor-airship/latest/interfaces/AirshipConfig.html). ## Test the integration After completing the setup, verify your integration: 1. **Build and run your app** on your iOS or Android device/simulator/emulator. 2. **Check the console output** for Airship channel creation: - **iOS**: Look for a log message in Xcode console: `Channel ID: ` - **Android**: Look for a log message in logcat: `Airship channel created: ` - The channel ID confirms successful SDK initialization. - For more detailed logging, see [Logging](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/logging/). If you see the channel ID in the console and no errors, your integration is successful. ## Next steps - [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/advanced-configuration/): Configure URL allowlists and other advanced settings - [Extending Airship](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/extending-airship/): Access native SDK features for advanced customization - [Push Notifications](https://www.airship.com/docs/developer/sdk-integration/capacitor/push-notifications/getting-started/): Configure push notifications - [Deep Links](https://www.airship.com/docs/developer/sdk-integration/capacitor/deep-links/): Handle deep links in your app If you don't see a channel ID or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/capacitor/troubleshooting/initialization/) for common problems and solutions. # Logging > Configure log levels and privacy settings to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. By default, the log level is set to **Info** for development builds and **Error** for production builds to ensure clean logs in a live environment. ## Log levels The following log levels are available, ordered from most to least verbose. | Log Level | Description | | :-------- | :---------- | | **Verbose** | Reports highly detailed SDK status, which is useful for deep debugging and troubleshooting. | | **Debug** | Reports general SDK status with more detailed information than `Info`. | | **Info** | Reports general SDK status and lifecycle events. | | **Warning** | Used for API deprecations, invalid setup, and other potentially problematic situations that are generally recoverable. | | **Error** | Used for critical errors, exceptions, and other situations that the SDK cannot gracefully handle. | | **None** | Disables all logging. | ## Configuring log levels You can set the log level in the Airship config options that are passed during takeOff. This setting acts as a minimum, and only logs at that level and higher will be logged. ```typescript // Available log levels: verbose, debug, info, warning, none await Airship.takeOff({ default: { logLevel: "verbose" }, ... }); ``` ## Log privacy levels For better security in production environments, you can control the visibility of log contents using privacy levels. This is especially useful when you need to debug a release build without exposing sensitive information. ### private (default) This is the default setting, designed to protect data in a production environment. ### public This setting increases log visibility, making it easier to capture detailed information from release builds. To ensure visibility in production builds, `verbose` and `debug` messages are automatically elevated to the `info` log level. ## Setting the privacy level You can set the privacy level in the Airship config options that are passed during takeOff. ```typescript await Airship.takeOff({ default: { logLevel: "verbose", ios: { logPrivacyLevel: "public" }, android: { logPrivacyLevel: "public" } }, ... }); ``` # Locale > Configure locale behavior and override the default locale that Airship uses. The Airship SDK is localized in 48 different languages for all strings included within the SDK. The strings within the app will automatically use the device's or app's configured [Locale](https://www.airship.com/docs/reference/glossary/#locale). Airship uses the user's locale for various locale-sensitive tasks, including selecting the language for messages and reporting for analytics. Apps can override the locale so that Airship uses a different locale than the device's current locale for messaging. ## Overriding the locale You can override the locale programmatically at runtime, which takes precedence over the device's locale settings. ```typescript await Airship.locale.setLocaleOverride("de") ``` ## Clearing the locale override To remove a locale override and return to using the device's locale: ```typescript await Airship.locale.clearLocaleOverride() ``` ## Getting the current locale To retrieve the locale that Airship is currently using: ```typescript const locale = await Airship.locale.getLocale() ``` # Advanced Configuration > Configure advanced settings like URL allowlists and other options for the Airship Capacitor plugin. ## URL Allowlist The URL allowlist controls which URLs the Airship SDK is able to act on. Configure the URL allowlist in your `takeOff` config options using the following properties: - `urlAllowListScopeOpenUrl`: Only URLs allowed for this scope can be opened from an action, displayed in landing page, displayed in an HTML in-app message, or displayed as media in an In-App Automation. Defaults to any Airship-originated URLs and YouTube URLs. - `urlAllowListScopeJavaScriptInterface`: These URLs are checked before the Airship JavaScript interface is injected into the webview. Defaults to any Airship-originated URLs. - `urlAllowList`: Both scopes are applied to these URLs. ```typescript await Airship.takeOff({ production: { appKey: "", appSecret: "" }, inProduction: true, site: "us", urlAllowList: ["*"], // Accept all URLs // Or configure specific scopes: urlAllowListScopeOpenUrl: ["https://example.com/*", "https://*.youtube.com/*"], urlAllowListScopeJavaScriptInterface: ["https://example.com/*"] }) ``` **Valid URL pattern syntax** ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` To accept any URL within the SDK, set the `urlAllowList` to `["*"]`. For a complete list of configuration options, see the [AirshipConfig reference](https://www.airship.com/docs/reference/libraries/capacitor-airship/latest/interfaces/AirshipConfig.html). # Extend Airship > How to extend the Airship Capacitor plugin to access native iOS and Android SDK features not exposed through the Capacitor API. You can provide a plugin extender that will be automatically loaded for the app. The extender can be used to modify the Airship config before SDK initialization and to access the underlying native SDK once Airship is ready. This gives the app a chance to customize parts of Airship that are not configurable through the Capacitor plugin, such as setting up [iOS Live Activities](https://www.airship.com/docs/developer/sdk-integration/capacitor/live-activities/) and [Android Live Updates](https://www.airship.com/docs/developer/sdk-integration/capacitor/live-updates/). ## iOS For iOS, create a Swift file named `AirshipPluginExtender.swift` and needs to be included in the main app target. Make sure the class has the `@objc(AirshipPluginExtender)` annotation and inherits `AirshipPluginExtenderProtocol`. ```swift import Foundation import AirshipKit import AirshipFrameworkProxy import ActivityKit @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { public static func onAirshipReady() { // Called when Airship is ready on the MainActor } public static func extendConfig(config: inout AirshipConfig) { // Called to extend the AirshipConfig before SDK initialization } } ``` ## Android Create a file in the App's src directory named `AirshipExtender`. It needs to extend `com.urbanairship.android.framework.proxy.AirshipPluginExtender` and have an empty constructor. ```kotlin // Replace with your package package com.example import android.content.Context import androidx.annotation.Keep import com.urbanairship.AirshipConfigOptions import com.urbanairship.UAirship import com.urbanairship.android.framework.proxy.AirshipPluginExtender @Keep public final class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { // Called when Airship is ready on a background thread. // Avoid doing long running, blocking work or it will delay Airship } override fun extendConfig( context: Context, configBuilder: AirshipConfigOptions.Builder ): AirshipConfigOptions.Builder { // Called to extend the AirshipConfig before SDK initialization return configBuilder } } ``` Register the extender in the manifest: ```xml ``` ### Push Notifications Configure and implement push notifications for iOS and Android platforms. # Push Notifications > How to configure your application to receive and respond to notifications. ## Platform Setup Before you can send and receive push notifications, you need to configure your app for the platform(s) you're targeting. Follow the platform-specific setup instructions below. ### iOS #### Enable Push Notifications Capability 1. Open your project in Xcode. 2. Click on your project in the Project Navigator. 3. Select your main app target and then click the **Signing & Capabilities** tab. 4. If you do not see Push Notifications enabled, click **+ Capability** and add **Push Notifications**. ![Adding the Push Notifications capability in Xcode](https://www.airship.com/docs/images/ios-enable-push-notifications-capabilities_hu_2e1789fffb02612b.webp) *Adding the Push Notifications capability in Xcode* #### Enable Background Modes 1. Select your main app target and then click the **Signing & Capabilities** tab. 2. Click **+ Capability** and add **Background Modes**. ![Adding the Background Modes capability in Xcode](https://www.airship.com/docs/images/ios-enable-background-mode-capabilities_hu_f135d9fec0ba0d06.webp) *Adding the Background Modes capability in Xcode* 3. In the **Background Modes** section, select the **Remote notifications** checkbox. ![Enabling Remote notifications in Background Modes](https://www.airship.com/docs/images/ios-background-mode-remote-notifications_hu_7e38b08288fcd7b2.webp) *Enabling Remote notifications in Background Modes* #### Notification Service Extension

To take advantage of notification attachments, such as images, animated gifs, and video, you will need to create a notification service extension.

Follow the steps in the [iOS Notification Service Extension Guide](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/). ### Android Configure Firebase Cloud Messaging (FCM) or Huawei Mobile Services (HMS) to enable push notifications on Android. #### FCM Setup Follow the [Android FCM setup instructions](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#fcm-setup). #### HMS Setup 1. Follow [Huawei's documentation](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-integrating-sdk-0000001050040084) to set up the HMS SDK. > **Note:** Airship requires HMS Core Push SDK 6.3.0.304 or newer. 2. Add `airshipHmsEnabled=true` to the app's gradle.properties. #### Notification Configuration Configure the notification icon and accent color in your `takeOff` config: ```typescript await Airship.takeOff({ production: { appKey: "", appSecret: "" }, development: { appKey: "", appSecret: "" }, inProduction: true, site: "us", android: { notificationConfig: { icon: "ic_notification", accentColor: "#00ff00" } } }) ``` See the [Capacitor Plugin Setup guide](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/getting-started/) for complete `takeOff` configuration options. ## Enable User Notifications The Airship SDK distinguishes between *user notifications* (visible to users) and *silent push notifications* (background data delivery). User notifications require explicit permission from the user. By default, user notifications are disabled. Enable them when you want to show visible notifications to users. ### Basic Enablement The simplest way to enable user notifications is with `setUserNotificationsEnabled()`: ```typescript await Airship.push.setUserNotificationsEnabled(true) ``` This will prompt the user for permission if not already granted. However, it does not provide feedback on whether the user accepted or denied the permission. > **Note:** For apps that target Android 13 (API 33) and above, enabling user notifications will display a runtime permission prompt. > > To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately on app startup, and instead wait for a more appropriate time to prompt for notification permission. ### Async Enablement with Fallback For more control over the permission flow, use `enableUserNotifications()` which returns the permission result and supports a fallback option: ```typescript // Enable with system settings fallback const granted = await Airship.push.enableUserNotifications({ fallback: "systemSettings" }) if (granted) { console.log('Notifications enabled') } else { console.log('Notifications denied') } ``` The `systemSettings` fallback option will prompt the user to open system settings if permission is denied on iOS or denied silently on Android. This gives users a second chance to enable notifications if they initially declined. ### Checking Notification Status To check if user notifications are currently enabled: ```typescript const enabled = await Airship.push.isUserNotificationsEnabled() ``` For more detailed status information, use `getNotificationStatus()`: ```typescript const status = await Airship.push.getNotificationStatus() console.log('Are notifications allowed:', status.areNotificationsAllowed) console.log('Is opted in:', status.isOptedIn) ``` To monitor notification status changes in real-time: ```typescript const handle = await Airship.push.onNotificationStatusChanged(event => { console.log('Notification status changed:', event.status) console.log('Is opted in:', event.status.isOptedIn) }) ``` ### Getting the Push Token To get the platform-specific push token (APNs token on iOS, FCM token on Android): ```typescript const token = await Airship.push.getPushToken() // Or listen for token updates const handle = await Airship.push.onPushTokenReceived(event => { console.log('Push token:', event.pushToken) }) ``` If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/capacitor/troubleshooting/push-notifications/) to check notification status and fix common issues. # Notification Events > How to handle push notification events, respond to user interactions, and manage active notifications. The Airship SDK provides event listeners for when a push is received or a notification is interacted with. ## Push Received Listen for when a push notification is received: ```typescript const handle = await Airship.push.onPushReceived(event => { console.log('Push received:', event.pushPayload) }) ``` This event fires when a push notification arrives, regardless of whether the app is in the foreground or background. ## Notification Response Listen for when a user interacts with a notification: ```typescript const handle = await Airship.push.onNotificationResponse(event => { console.log('Notification tapped:', event) console.log('Action ID:', event.actionId) if (event.actionId === 'custom_action') { // Handle custom action } }) ``` This event fires when a user taps on a notification or a notification action button. ## Managing Active Notifications You can retrieve and clear notifications that are currently displayed in the notification center. ### Get Active Notifications Retrieve the list of currently displayed notifications: ```typescript const notifications = await Airship.push.getActiveNotifications() console.log('Active notifications:', notifications) ``` > **Note:** On Android, this list only includes notifications sent through Airship. ### Clear Notifications Clear all notifications for the app: ```typescript await Airship.push.clearNotifications() ``` Clear a specific notification by identifier: ```typescript await Airship.push.clearNotification(identifier) ``` > **Note:** On Android, you can use this method to clear notifications outside of Airship. The identifier is in the format `:`. # Customize Notifications > How to customize push notification presentation, badges, quiet time, and foreground display behavior. ## iOS Notification Options By default, the Airship SDK will request `alert`, `badge`, and `sound` notification options for remote notifications. This can be configured by setting notification options before enabling user notifications. ```typescript await Airship.push.iOS.setNotificationOptions([ "alert", "badge", "sound" ]) ``` ### Provisional Authorization Apps can request provisional authorization along with the usual notification options. When requesting provisional authorization, apps do not need to prompt the user for permission initially, and notifications will be delivered in a non-interruptive manner to the Notification Center until the user explicitly chooses to keep delivering messages either prominently or quietly. ```typescript await Airship.push.iOS.setNotificationOptions([ "alert", "badge", "sound", "provisional" ]) ``` ### Foreground Presentation Options When a push is received in the foreground on iOS, how the notification is displayed to the user is controlled by foreground presentation options. By default, the SDK will not set any options so the notification will be silenced. ```typescript await Airship.push.iOS.setForegroundPresentationOptions([ "banner", "list", "sound" ]) ``` ### Badges The badge on iOS presents a counter on top of the application icon. You can control this directly through Airship. ```typescript // Set badge number await Airship.push.iOS.setBadgeNumber(20) // Reset badge await Airship.push.iOS.setBadgeNumber(0) // Enable auto-badge await Airship.push.iOS.setAutobadgeEnabled(true) ``` > **Important:** When using auto-badge, only modify the badge value through Airship methods to ensure the value stays in sync. ### Quiet Time Quiet time allows you to suppress notifications during specific hours. Notifications are still received but won't be displayed to the user during the quiet time window. ```typescript // Set quiet time hours await Airship.push.iOS.setQuietTime({ startHour: 22, startMinute: 0, endHour: 7, endMinute: 0 }) // Enable quiet time await Airship.push.iOS.setQuietTimeEnabled(true) ``` > **Note:** Quiet time is only supported on iOS. ## Android Foreground Notifications By default, push notifications received while the app is in the foreground on Android are displayed to the user. You can control this behavior globally: ```typescript // Disable foreground notifications await Airship.push.android.setForegroundNotificationsEnabled(false) // Enable foreground notifications await Airship.push.android.setForegroundNotificationsEnabled(true) ``` ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. For Android, all push messages are delivered in the background, but default Airship will treat messages without an `alert` as silent. For iOS, set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). > **Note:** Pushes sent with the `content_available` property (iOS) or without an `alert` (Android) do not have guaranteed delivery. Factors affecting delivery include battery life, whether the device is connected to WiFi, and the number of silent pushes sent within a recent time period. These metrics are determined solely by iOS/Android and APNs/FCM. Therefore, this feature is best used for supplementing the regular behavior of the app rather than providing critical functionality. For instance, an app could use a silent push to pre-fetch new data ahead of time in order to reduce load times when the app is later launched by the user. ### In-App Experiences Configure and control In-App Experiences in Capacitor applications. # In-App Experiences > Pause, resume, and control display timing for In-App Experiences. In-App Experiences are automatically enabled when you integrate the Airship SDK. Use these methods to control when and how they are displayed. ## Pausing and Resuming Display You can pause and resume In-App Experiences to control when they are displayed to users. ```typescript // Pause in-app experiences await Airship.inApp.setPaused(true) // Resume in-app experiences await Airship.inApp.setPaused(false) // Check if paused const isPaused = await Airship.inApp.isPaused() ``` ### Auto-Pause on Launch You can configure the SDK to automatically pause In-App Experiences on launch. This is useful if you want to defer showing In-App Experiences until after onboarding or other critical app flows. ```typescript await Airship.takeOff({ production: { appKey: "", appSecret: "" }, development: { appKey: "", appSecret: "" }, inProduction: true, site: "us", autoPauseInAppAutomationOnLaunch: true }) ``` See the [Capacitor Plugin Setup guide](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/getting-started/) for complete `takeOff` configuration options. When you're ready to display In-App Experiences, call `setPaused(false)`: ```typescript await Airship.inApp.setPaused(false) ``` ## Display Interval Control the minimum time between In-App Experience displays to avoid overwhelming users. ```typescript // Set display interval to 5 seconds await Airship.inApp.setDisplayInterval(5000) // Get current display interval const interval = await Airship.inApp.getDisplayInterval() ``` The display interval is the minimum time (in milliseconds) that must pass between displaying In-App Experiences. # Custom Views > Register custom native views to use within Scenes. A *Custom View* is a native view from your mobile or web application embedded into a Scene. Custom Views can display any native content your app exposes, so you can reuse that existing content within any screen in a Scene. Custom Views allow you to embed native iOS and Android views within Scenes, giving you full control over design and layout while leveraging Airship's targeting and orchestration capabilities. ## Requirements To use Custom Views in Capacitor, you must extend the native Airship modules using the `AirshipPluginExtender`. See [Extending Airship](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/extending-airship/) for setup instructions. ## Registering Custom Views Custom Views must be registered on each native platform separately: ### iOS See the [Apple Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/custom-views/) for detailed implementation instructions. Custom Views should be registered after takeOff in your native iOS code: ```swift import AirshipKit @objc(AirshipExtender) class AirshipExtender: NSObject, AirshipPluginExtenderDelegate { func onAirshipReady() { // Register custom views AirshipCustomViewManager.shared.register(name: "my-custom-view") { args in // Return your SwiftUI view MyCustomView(args: args) } } } ``` ### Android See the [Android Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/custom-views/) for detailed implementation instructions. Custom Views should be registered during the `onAirshipReady` callback in your native Android code: ```kotlin import com.urbanairship.AirshipCustomViewManager class AirshipExtender : AirshipPluginExtender { override fun onAirshipReady(context: Context) { // Register custom views AirshipCustomViewManager.register("my-custom-view") { context, args -> // Return your Android View MyCustomView(context, args) } } } ``` ## Using Custom Views Once registered, Custom Views can be added to Scenes in the Airship dashboard: 1. Create or edit a Scene 2. Add the **Custom View** content element to a screen 3. Enter the view name (e.g., `my-custom-view`) that matches the name you registered in your native code 4. Optionally add key-value pairs to pass custom properties to the view The native view will be displayed within the Scene with the properties you configured. ## Example: Embedding Capacitor Web Views A powerful use case for Custom Views in Capacitor is embedding your Capacitor web app (or a subset of it) directly within a Scene. This allows you to display specific routes or pages from your web app inside an Airship Scene. ### iOS Implementation ```swift import Foundation import AirshipFrameworkProxy import Capacitor import AirshipCore import SwiftUI import UIKit @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { @MainActor public static func onAirshipReady() { AirshipCustomViewManager.shared.register(name: "capacitor") { args in CapView( startPath: args.properties?.object?["path"]?.string ) .edgesIgnoringSafeArea(.all) .frame(maxWidth: .infinity, maxHeight: .infinity) } } } struct CapView: UIViewControllerRepresentable { let startPath: String? func makeUIViewController(context: Context) -> CustomCapViewController { let controller = CustomCapViewController() controller.startPath = startPath return controller } func updateUIViewController(_ uiViewController: CustomCapViewController, context: Context) { // Handle updates (if necessary) } class CustomCapViewController: CAPBridgeViewController { var startPath: String? = nil override func instanceDescriptor() -> InstanceDescriptor { let descriptor = super.instanceDescriptor() descriptor.appStartPath = startPath return descriptor } } } ``` ### Android Implementation First, create a layout file at `res/layout/custom_view.xml`: ```xml ``` Then, register the custom view in your `AirshipExtender.kt`: ```kotlin package com.example.plugin import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.annotation.Keep import androidx.appcompat.app.AppCompatActivity import androidx.core.view.doOnAttach import androidx.fragment.app.Fragment import com.getcapacitor.Bridge import com.getcapacitor.CapConfig import com.getcapacitor.Logger import com.getcapacitor.PluginLoadException import com.getcapacitor.PluginManager import com.urbanairship.UAirship import com.urbanairship.android.framework.proxy.AirshipPluginExtender import com.urbanairship.android.layout.AirshipCustomViewArguments import com.urbanairship.android.layout.AirshipCustomViewHandler import com.urbanairship.android.layout.AirshipCustomViewManager import com.urbanairship.json.optionalField @Keep class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { AirshipCustomViewManager.register("capacitor", CustomCapViewHandler()) } } class CustomCapViewHandler: AirshipCustomViewHandler { override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View { return CapView(context).also { it.startPath = args.properties.optionalField("path") } } } class CapView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { var startPath: String? = null init { id = View.generateViewId() val fm = (context as AppCompatActivity).supportFragmentManager fm.findFragmentById(id)?.let { fm.beginTransaction() .remove(it) .commit() } doOnAttach { findViewById(id)?.let { fm.beginTransaction() .setReorderingAllowed(true) .add(id, CapFragment.newInstance(startPath)) .commitNowAllowingStateLoss() } } } } class CapFragment: Fragment() { private var bridge: Bridge? = null private val path: String? by lazy { arguments?.getString(ARG_PATH) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.custom_view, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) load(savedInstanceState) } override fun onStart() { super.onStart() bridge?.onStart() } override fun onResume() { super.onResume() bridge?.app?.fireStatusChange(true) bridge?.onResume() } override fun onPause() { super.onPause() bridge?.app?.fireStatusChange(false) bridge?.onPause() } override fun onStop() { super.onStop() bridge?.onStop() } override fun onDestroy() { super.onDestroy() bridge?.onDestroy() bridge?.onDetachedFromWindow() } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) bridge?.onConfigurationChanged(newConfig) } private fun load(savedInstanceState: Bundle?) { if (bridge == null) { bridge = Bridge.Builder(this) .apply { try { val loader = PluginManager(requireActivity().assets) addPlugins(loader.loadPluginClasses()) } catch (ex: PluginLoadException) { Logger.error("Error loading plugins.", ex) } } .setConfig( CapConfig.Builder(context) .setStartPath(this.path) .create() ) .setInstanceState(savedInstanceState) .create() } } companion object { const val ARG_PATH: String = "path" @JvmStatic @JvmOverloads fun newInstance(path: String? = null): CapFragment = CapFragment().apply { arguments = Bundle().apply { path?.let { putString(ARG_PATH, it) } } } } } ``` ### Usage in Scenes When creating a Scene in the Airship dashboard: 1. Add a **Custom View** content element 2. Set the view name to `capacitor` 3. Optionally add a `path` property to specify which route in your Capacitor app to display (e.g., `/products`, `/settings`) > **Note:** **Sizing limitation**: The custom view must use hard-coded sizing (percentages or pixels). Auto-sizing is not currently supported for embedded Capacitor views. This implementation creates a fully functional Capacitor web view within the Scene, complete with all your Capacitor plugins and routing capabilities. The optional `path` property allows you to display specific routes or pages from your web app. ### Message Center Implement Message Center to provide an inbox for rich HTML-based messages. # Message Center > The default Message Center is available for Capacitor with minimal integration required. Basic theming options are supported. Message Center provides an inbox for rich, HTML-based messages. Learn more about Message Center in our [feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Display the Message Center Display the Message Center with a single method call: ```js await Airship.messageCenter.display() ``` To build a custom message list, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/capacitor/message-center/embedding/). Individual messages will still display as a native overlay. ## Fetch Messages Retrieve messages from the inbox: ```js const messages = await Airship.messageCenter.getMessages() ``` ## Listen for Message Updates Subscribe to message updates using event listeners: ```js Airship.addListener('messageCenterUpdated', async () => { const messages = await Airship.messageCenter.getMessages(); // Handle messages }); ``` ## Listen for Unread Count Changes Subscribe to unread count updates: ```js const unreadCount = await Airship.messageCenter.getUnreadCount(); // Update badge or UI ``` ## Refresh Messages Manually refresh the message list from the server: ```js await Airship.messageCenter.refreshMessages() ``` ## Mark Messages as Read Mark one or more messages as read: ```js await Airship.messageCenter.markMessageRead("message-id") ``` ## Delete Messages Delete one or more messages: ```js await Airship.messageCenter.deleteMessage("message-id") ``` # Embed the Message Center > Create custom Message Center lists with full control over design and navigation. This guide covers creating custom Message Center list implementations for Capacitor applications. You can build a custom message list using web technologies, but individual messages must be displayed using the native overlay. > **Note:** Capacitor does not support embedding native message views. Use `showMessageView()` to display individual messages as an overlay. ## Override Default Display Behavior To use a custom Message Center list instead of the default UI, disable auto-launch and add a listener to handle display events: ```js // Disable the default UI await Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false) // Add a listener to handle display events Airship.messageCenter.onDisplay(event => { if (event.messageId) { // Show specific message in native overlay await Airship.messageCenter.showMessageView(event.messageId); } else { // Navigate to your custom message list navigateToCustomMessageList(); } }); ``` ## Displaying Individual Messages Use `showMessageView` to display messages in a native overlay: ```js await Airship.messageCenter.showMessageView("message-id") ``` ### Preference Center Implement Preference Center to let users control their subscription preferences. # Preference Center > Preference Center allows users to opt in and out of subscription lists configured via the Airship Dashboard. > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. Preference Center provides a pre-built UI for users to manage their subscription preferences. Learn more in the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Display a Preference Center Display a Preference Center with a single method call: ```js await Airship.preferenceCenter.display("preference-center-id") ``` To build a custom Preference Center UI, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/capacitor/preference-center/embedding/). # Embed the Preference Center > Create custom Preference Center UIs by fetching the config and building your own subscription management interface. This guide covers creating custom Preference Center UIs for Capacitor applications. Unlike the default Preference Center, you'll build your own UI from scratch using the Preference Center configuration and subscription list APIs. ## Override Default Display Behavior To use a custom Preference Center instead of the default UI, disable auto-launch for the specific Preference Center ID and handle display events: ```js // Disable the OOTB UI for this Preference Center await Airship.preferenceCenter.setAutoLaunchDefaultPreferenceCenter( "preference-center-id", false ); // Add a listener to handle display events Airship.preferenceCenter.onDisplay(event => { const preferenceCenterId = event.preferenceCenterId; // Navigate to your custom preference center UI navigateToCustomPreferenceCenter(preferenceCenterId); }); ``` ## Fetching Preference Center Config The Preference Center config contains all the information needed to build your UI, including subscription lists, sections, and display settings. ```js const config = await Airship.preferenceCenter.getConfig("preference-center-id"); ``` > **Note:** The config might not be available immediately on first app start. Implement exponential backoff if automatically retrying, or provide a UI for users to manually retry. ## Building Your Custom UI You'll need to: 1. **Fetch the config** to get the list of subscription lists and their current state 2. **Build your UI** using the config data (sections, subscription lists, display settings) 3. **Update subscription lists** when users make changes using the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/capacitor/audience/subscription-lists/) ### Example Implementation ```js async function loadPreferenceCenter(preferenceCenterId) { try { const config = await Airship.preferenceCenter.getConfig(preferenceCenterId); // Build your UI with the config const container = document.getElementById('preference-center'); // Display title const title = document.createElement('h1'); title.textContent = config.display.name; container.appendChild(title); // Display sections config.sections.forEach((section) => { const sectionDiv = document.createElement('div'); const sectionTitle = document.createElement('h2'); sectionTitle.textContent = section.display.name; sectionDiv.appendChild(sectionTitle); // Display items (subscription lists) section.items.forEach((item) => { const itemDiv = document.createElement('div'); const label = document.createElement('label'); label.textContent = item.display.name; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = await isSubscribed(item.subscriptionId); checkbox.addEventListener('change', async (e) => { if (e.target.checked) { await Airship.contact.subscriptionLists.subscribe(item.subscriptionId); } else { await Airship.contact.subscriptionLists.unsubscribe(item.subscriptionId); } }); itemDiv.appendChild(checkbox); itemDiv.appendChild(label); sectionDiv.appendChild(itemDiv); }); container.appendChild(sectionDiv); }); } catch (error) { console.error('Failed to load Preference Center:', error); } } ``` > **Important:** Preference Center configuration is currently limited to subscription lists only. Use the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/capacitor/audience/subscription-lists/) to manage user subscriptions. ### Audience Management Integrate audience management features into your Capacitor app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs and listen for channel creation. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. ```js // Get the channel ID (may return null if not yet created) const channelID = await Airship.channel.getChannelId() // Wait for the channel ID to be created (returns the channel ID once available) const channelID = await Airship.channel.waitForChannelId() ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Use `waitForChannelId()` if you need to wait for the channel to be created before proceeding. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. ```js await Airship.channel.onChannelCreated(event => { console.log('Channel created: ' + event.channelId); }); ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring the takeOff options, see [Capacitor SDK Setup](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/getting-started/). ```js await Airship.takeoff({ ... isChannelCaptureEnabled: false, }) ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during takeOff. For more information about Privacy Manager, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. ```js await Airship.contact.identify(namedUserId) ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. ```js await Airship.contact.reset() ``` You can get the Named User ID only if you set it through the SDK. ```js const namedUser = await Airship.contact.getNamedUserId() ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. ```js Airship.channel.editTags() .addTags(["one", "two", "three"]) .removeTags(["some_tag"]) .apply() // Accessing channel tags const tags = await Airship.channel.getTags() ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```js Airship.channel.editTagGroups() .addTags("loyalty", ["silver-member"]) .removeTags("loyalty", ["bronze-member"]) .apply() ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```js Airship.contact.editTagGroups() .addTags("loyalty", ["silver-member"]) .removeTags("loyalty", ["bronze-member"]) .apply() ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. ```js Airship.channel.editAttributes() .setAttribute("device_name", "Bobby's Phone") .setAttribute("average_rating", 4.99) .removeAttribute("vip_status") .apply() ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. ```js Airship.contact.editAttributes() .setAttribute("first_name", "Bobby") .apply() ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. ```js Airship.contact.editAttributes() .setJsonAttribute("attribute_name", "instance_id", {"key":"value", "another_key":"another_value"}) .removeJsonAttribute("some_attribute_name", "some_instance_id") .apply() ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. ```js // Modifying channel subscription lists Airship.channel.editSubscriptionLists() .subscribe("food") .unsubscribe("sports") .apply() // Fetching channel subscription lists const channelSubscriptions = await Airship.channel.getSubscriptionLists() ``` ## Contact Subscription Lists Contact subscriptions are set at the user-level and require a Channel scope specifying the types that the subscription list applies to. ```js // Modifying contact subscription lists Airship.contact.editSubscriptionLists() .subscribe("food", "app") .unsubscribe("sports", "sms") .apply() // Fetching contact subscription lists const contactSubscriptions = await Airship.contact.getSubscriptionLists() ``` ## Verifying Subscription Lists To verify that subscription lists have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ### Data Collection Overview of data collection and controls provided by the Airship Capacitor SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: | Privacy Manager Flag | Features | |----------------------|----------| | `push` | Push notifications | | `in_app_automation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | | `message_center` | Message Center | | `tags_and_attributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | | `contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | | `analytics` | Associated identifiers, Custom events, Screen tracking, Surveys, email address, Feature Flag interaction | | `feature_flags` | Feature Flag evaluation and interaction | | `all` | All features | | `none` | No features | ## Configuring default enabled features You can configure which features are enabled by default when the SDK initializes. This is done in your Airship config during `takeOff`. For information about setting up the Airship SDK and configuring the takeOff options, see [Capacitor SDK Setup](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/getting-started/). ### Default configuration (all features enabled) By default, all features are enabled. The SDK will collect data and make network requests as configured. ### Disabling all features To disable all features by default (useful for consent opt-in flows), set the enabled features to an empty array: ```typescript await Airship.takeOff({ default: { enabledFeatures: [] }, ... }); ``` ### Enabling specific features To enable only specific features by default: ```typescript await Airship.takeOff({ default: { enabledFeatures: ["push", "analytics"] }, ... }); ``` ## Modifying enabled features at runtime You can enable or disable features at any time after takeOff. Once you modify the enabled features from the default, those settings are persisted between app launches. ### Enabling features ```typescript await Airship.privacyManager.enableFeatures(["push", "analytics"]); ``` ### Disabling features ```typescript await Airship.privacyManager.disableFeatures(["push", "analytics"]); ``` ## Consent opt-in flow example A common use case is to start with all features disabled, then enable them as users grant consent: ```typescript // Start with all features disabled await Airship.takeOff({ default: { enabledFeatures: [] }, ... }); // Later, when user grants consent: async function userGrantedConsent() { // Enable features based on user's consent choices await Airship.privacyManager.enableFeatures(["push", "analytics"]); } ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Apple Privacy Manifest](https://www.airship.com/docs/reference/data-collection/apple-privacy-manifest/) - Declare data collection practices to Apple (iOS) - [Google Play Data Safety](https://www.airship.com/docs/reference/data-collection/google-play-data-safety/) - Reference for Google Play's Data Safety section (Android) - [Analytics](https://www.airship.com/docs/developer/sdk-integration/capacitor/data-collection/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/capacitor/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with custom events. They require enabling analytics for your app. For detailed information, see the [Custom Events guide](https://www.airship.com/docs/guides/audience/events/custom-events/). ```typescript let event = { eventName: "event_name", eventValue: 123.12, properties: { "my_custom_property": "some custom value", "is_neat": true, "any_json": { "foo": "bar" } } } await Airship.analytics.addCustomEvent(event) ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. ```typescript await Airship.analytics.associateIdentifier("key", "value") ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. ```typescript await Airship.analytics.trackScreen("MainScreen") ``` ### Troubleshooting Common issues and solutions for Airship plugin setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/getting-started/) or [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/advanced-configuration/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Installation errors If you encounter errors during installation: - Verify that you're using a compatible version of Capacitor. See [Requirements](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/getting-started/#requirements) in *Getting Started*. - Ensure all native dependencies are properly synced with `npx cap sync`. - Check that your iOS and Android projects are correctly configured. ## Initialization errors If you encounter errors during SDK initialization: - Verify your credentials in the Airship dashboard. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. - Check that `takeOff` is called correctly in your app or configured in `capacitor.config.json`. - Ensure both iOS and Android native modules are properly installed. # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/capacitor/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. ## Push Notifications Not Working If push notifications are not being received: - Verify that push notifications are enabled for both iOS and Android. - Check that APNs (iOS) and FCM (Android) are properly configured. - Ensure the app has notification permissions. ## Checking Push Notification Status If you're having trouble with push notifications, check the notification status to see the current state: ```typescript const status = await Airship.push.getNotificationStatus() console.log('Are notifications allowed:', status.areNotificationsAllowed) console.log('Is opted in:', status.isOptedIn) console.log('Is user notifications enabled:', status.isUserNotificationsEnabled) console.log('Is airship opted in:', status.isAirshipOptedIn) console.log('Is user opted in:', status.isUserOptedIn) console.log('Is pushable:', status.isPushable) console.log('Is privacy manager enabled:', status.isPrivacyManagerEnabled) ``` You can also listen for status changes to debug permission issues: ```typescript const handle = await Airship.push.onNotificationStatusChanged(event => { console.log('Notification status changed:', event.status) if (!event.status.areNotificationsAllowed) { console.log('System-level notifications are disabled') } if (!event.status.isUserNotificationsEnabled) { console.log('User notifications are disabled in Airship') } if (!event.status.isPushable) { console.log('Device is not pushable') } }) ``` ## Cordova Integrate the Airship SDK into your Cordova applications for iOS and Android. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Message Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/cordova/message-center/getting-started/) and [Preference Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/cordova/preference-center/getting-started/). ```js Airship.onDeepLink((event) => { var deepLink = event.deepLink; // Handle deep link }); ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Running Actions You can run actions programmatically using the `Airship.actions.run()` method. The action value can be a string, number, boolean, null, object, or array. **Running an action** ```js // Run an action with a string value Airship.actions.run("action_name", "action_value", function(result) { // Action completed successfully console.log("Action result:", result); }, function(error) { // Action failed console.error("Action error:", error); } ); // Run an action with an object value Airship.actions.run("action_name", { key: "value", number: 42 }, function(result) { console.log("Action result:", result); } ); // Run an action without a value Airship.actions.run("action_name", null, function(result) { console.log("Action result:", result); } ); ``` # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. ```js Airship.featureFlagManager.flag("YOUR_FLAG_NAME", (flag) => { if (flag.isEligible) { // Do something with the flag } else { // Disable feature or use default behavior } }); ``` ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/cordova/data-collection/privacy-manager/). ```js Airship.featureFlagManager.trackInteraction(flag); ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. ```js Airship.featureFlagManager.flag( "another_rad_flag", (flag) => { // do something with the flag }, (error) => { console.log("error: " + error) } ); ``` # Cordova Plugin Changelog > The latest updates to the Airship Cordova plugin. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 19.0.0 May 11, 2026 Major release that migrates iOS to Swift Package Manager, drops CocoaPods support, bumps the minimum Cordova platform versions, and adds Live Activity (iOS) and Live Update (Android) JS hooks. ### Changes - iOS now uses Swift Package Manager instead of CocoaPods. Requires `cordova-ios` 8.0.0 or newer. - Android now requires `cordova-android` 15.0.0 or newer. - Added `Airship.liveActivityManager` (iOS) with `list`, `listAll`, `start`, `update`, `end`, and `onLiveActivitiesUpdated` listener. Requires iOS 16.1+ and a host-app call to `LiveActivityManager.shared.setup` to register `ActivityAttributes` types. - Added `Airship.liveUpdateManager` (Android) with `list`, `listAll`, `start`, `update`, `end`, and `clearAll`. ## 18.0.0 January 23, 2026 Major release that updates to iOS and Android SDK 20.1.1, improves accessibility, and fixes a potential crash in Android Scenes. ### Changes - Updated Android SDK to [20.1.1](https://github.com/urbanairship/android-library/releases/tag/20.1.1) - Updated iOS SDK to [20.1.1](https://github.com/urbanairship/ios-library/releases/tag/20.1.1) - Improved VoiceOver focus handling for Message Center on iOS - Fixed a potential crash in Android Scenes and addressed various accessibility issues - Requires cordova-android 14.0.0+ - Requires AGP 8.13+ and Kotlin 2.2+ for Android builds - Requires iOS deployment target 16.0+ ## 17.5.1 November 15, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Updated Android SDK to [19.13.6](https://github.com/urbanairship/android-library/releases/tag/19.13.6) - Updated iOS SDK to [19.11.2](https://github.com/urbanairship/ios-library/releases/tag/19.11.2) ## 17.5.0 August 27, 2025 Patch release that updates the Android SDK to 19.11.0 and the iOS SDK to 19.8.3 ### Changes - Updated Android SDK to [19.11.0](https://github.com/urbanairship/android-library/releases/tag/19.11.0) - Updated iOS SDK to [19.8.3](https://github.com/urbanairship/ios-library/releases/tag/19.8.3) - Updated HMS to 6.13.0.300 ## 17.4.0 July 31, 2025 Minor release that updates the Android SDK to 19.10.0 and the iOS SDK to 19.7.0 ### Changes - Updated Android SDK to [19.10.0](https://github.com/urbanairship/android-library/releases/tag/19.10.0) - Updated iOS SDK to [19.7.0](https://github.com/urbanairship/ios-library/releases/tag/19.7.0) ## 17.3.0 June 26, 2025 Minor release that updates the Android SDK to 19.9.1 and the iOS SDK to 19.6.1 ### Changes - Updated Android SDK to [19.9.1](https://github.com/urbanairship/android-library/releases/tag/19.9.1) - Updated iOS SDK to [19.6.1](https://github.com/urbanairship/ios-library/releases/tag/19.6.1) ## 17.2.1 May 9, 2025 Patch release that updates iOS SDK to 19.3.2 ### Changes - Updated iOS SDK to [19.3.2](https://github.com/urbanairship/ios-library/releases/tag/19.3.2) ## 17.1.0 March 27, 2025 Minor release that updates the Android SDK to 19.4.0 and the iOS SDK to 19.1.1 ### Changes - Updated Android SDK to [19.4.0](https://github.com/urbanairship/android-library/releases/tag/19.4.0) - Updated iOS SDK to [19.1.1](https://github.com/urbanairship/ios-library/releases/tag/19.1.1) ## 17.0.0 February 12, 2025 Major release that updates the Android Airship SDK to 19.1.0 and iOS Airship SDK to 19.0.3 ### Changes - Updated Android SDK to [19.1.0](https://github.com/urbanairship/android-library/releases/tag/19.1.0). - Updated iOS SDK to [19.0.3](https://github.com/urbanairship/ios-library/releases/tag/19.0.3). - iOS requires Xcode 16.2, iOS 15+, and cordova-ios 7.1.0+ - Android requires compileSdkVersion 35+, minSdkVersion 23+, and cordova-android 13.0+ ## 16.0.0 July 4, 2024 Minor release that updates dependencies and adds a new config option for logging on iOS. ### Changes - Updated Airship Android SDK to 18.1.1 - Updated Airship iOS SDK to 18.5.0 - Added iOS logPrivacyLevel that can be set in the environments when calling takeOff ## 15.2.4 June 21, 2024 Patch release to fix a regression on iOS with In-App Automations, Scenes, and Surveys ignoring screen, version, and custom event triggers. Apps using those triggers that are on 15.2.3 should update. ### Changes - Updated iOS SDK to 18.4.1 - Fixed regression with triggers ## 15.2.3 June 20, 2024 Patch release with several bug fixes. ### Changes - Updated iOS SDK to 18.4.0. - Fixed compatibility with cordova-android@13. ## 15.2.2 May 17, 2024 Patch release that updates to latest Airship SDKs. ### Changes - Updated iOS SDK to 18.2.2 [View Older Releases](https://github.com/urbanairship/urbanairship-cordova/releases?q=created%3A%3C2024-05-15&expanded=true) # Cordova Plugin Resources > API documentation, source code, and changelogs for the Airship Cordova plugin. ## Platform Support {#platform-support} | Feature | iOS | Android | |----------------------------------------|-----|---------| | Push Notifications | ✅ | ✅ | | Live Activities | ❌ | ❌ | | Live Updates | ❌ | ❌ | | In-App Experiences | ✅ | ✅ | | Custom Views | ✅ | ✅ | | Embedded Content | ❌ | ❌ | | Message Center | ✅ | ✅ | | Preference Center | ✅ | ✅ | | Feature Flags | ✅ | ✅ | | Analytics | ✅ | ✅ | | Contacts | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ## API References * [Cordova API Documentation](https://www.airship.com/docs/reference/libraries/urbanairship-cordova/latest/) ## GitHub Samples * [Cordova Sample App](https://github.com/urbanairship/urbanairship-cordova/tree/main/Example) ## Source * [Source](https://github.com/urbanairship/urbanairship-cordova) ## Changelog * [Cordova Changelog](https://www.airship.com/docs/developer/sdk-integration/cordova/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [Cordova License](https://github.com/urbanairship/urbanairship-cordova/blob/main/LICENSE) ### SDK Installation Complete installation and configuration guides for the Airship Cordova plugin. # Install and Set Up the Cordova Plugin > How to install the Airship Cordova plugin.## Requirements - cordova-ios 8.0.0 or higher - cordova-android 15.0.0 or higher - iOS 16+ (Xcode 26+) - Android API 23+ ## Setup Install the plugin using Cordova CLI: ```bash cordova plugin add @ua/cordova-airship ``` For HMS support, you will also need the HMS module: ```bash cordova plugin add @ua/cordova-airship-hms ``` ### iOS Modify the app's config.xml to enable swift support and set the min iOS version: ```xml ``` ## Initialize Airship Before you can access any of the module's API, the Airship module needs to be initialized. This can be accomplished by calling [takeOff](https://www.airship.com/docs/reference/libraries/urbanairship-cordova/latest/interfaces/Airship.html#takeOff) when the device is ready. **Calling takeOff** ```javascript // TakeOff Airship.takeOff({ production: { appKey: "", appSecret: "" }, development: { appKey: "", appSecret: "" }, inProduction: true, site: "us", // use "eu" for EU cloud projects urlAllowList: ["*"], android: { notificationConfig: { icon: "ic_notification", accentColor: "#00ff00" } } }) ``` Takeoff can be called multiple times, but the config passed in `takeOff` won't be applied until the next app init. For a complete list of configuration options, see the [AirshipConfig reference](https://www.airship.com/docs/reference/libraries/urbanairship-cordova/latest/interfaces/AirshipConfig.html). ## Test the integration After completing the setup, verify your integration: 1. **Build and run your app** on your iOS or Android device/simulator/emulator. 2. **Check the console output** for Airship channel creation: - **iOS**: Look for a log message in Xcode console: `Channel ID: ` - **Android**: Look for a log message in logcat: `Airship channel created: ` - The channel ID confirms successful SDK initialization. - For more detailed logging, see [Logging](https://www.airship.com/docs/developer/sdk-integration/cordova/installation/logging/). If you see the channel ID in the console and no errors, your integration is successful. ## Next steps - [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/cordova/installation/advanced-configuration/): Configure URL allowlists and other advanced settings - [Push Notifications](https://www.airship.com/docs/developer/sdk-integration/cordova/push-notifications/getting-started/): Configure push notifications - [Deep Links](https://www.airship.com/docs/developer/sdk-integration/cordova/deep-links/): Handle deep links in your app If you don't see a channel ID or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/cordova/troubleshooting/initialization/) for common problems and solutions. # Logging > Configure log levels and privacy settings to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. By default, the log level is set to **Info** for development builds and **Error** for production builds to ensure clean logs in a live environment. ## Log levels The following log levels are available, ordered from most to least verbose. | Log Level | Description | | :-------- | :---------- | | **Verbose** | Reports highly detailed SDK status, which is useful for deep debugging and troubleshooting. | | **Debug** | Reports general SDK status with more detailed information than `Info`. | | **Info** | Reports general SDK status and lifecycle events. | | **Warning** | Used for API deprecations, invalid setup, and other potentially problematic situations that are generally recoverable. | | **Error** | Used for critical errors, exceptions, and other situations that the SDK cannot gracefully handle. | | **None** | Disables all logging. | ## Configuring log levels You can set the log level in the Airship config options that are passed during takeOff. This setting acts as a minimum, and only logs at that level and higher will be logged. ```javascript // Available log levels: verbose, debug, info, warning, none Airship.takeOff({ default: { logLevel: "verbose" }, ... }); ``` ## Log privacy levels For better security in production environments, you can control the visibility of log contents using privacy levels. This is especially useful when you need to debug a release build without exposing sensitive information. ### private (default) This is the default setting, designed to protect data in a production environment. ### public This setting increases log visibility, making it easier to capture detailed information from release builds. To ensure visibility in production builds, `verbose` and `debug` messages are automatically elevated to the `info` log level. ## Setting the privacy level You can set the privacy level in the Airship config options that are passed during takeOff. ```javascript Airship.takeOff({ default: { logLevel: "verbose", ios: { logPrivacyLevel: "public" }, android: { logPrivacyLevel: "public" } }, ... }); ``` # Locale > Configure locale behavior and override the default locale that Airship uses. The Airship SDK is localized in 48 different languages for all strings included within the SDK. The strings within the app will automatically use the device's or app's configured [Locale](https://www.airship.com/docs/reference/glossary/#locale). Airship uses the user's locale for various locale-sensitive tasks, including selecting the language for messages and reporting for analytics. Apps can override the locale so that Airship uses a different locale than the device's current locale for messaging. ## Overriding the locale You can override the locale programmatically at runtime, which takes precedence over the device's locale settings. ```javascript Airship.locale.setLocaleOverride("de") ``` ## Clearing the locale override To remove a locale override and return to using the device's locale: ```javascript Airship.locale.clearLocaleOverride(); ``` ## Getting the current locale To retrieve the locale that Airship is currently using: ```javascript Airship.locale.getCurrentLocale((locale) => { console.log('Current locale: ', locale); }); ``` # Advanced Configuration > Configure advanced settings like URL allowlists and other options for the Airship Cordova plugin. ## URL Allowlist The URL allowlist controls which URLs the Airship SDK is able to act on. Configure the URL allowlist in your `takeOff` config options using the following properties: - `urlAllowListScopeOpenUrl`: Only URLs allowed for this scope can be opened from an action, displayed in landing page, displayed in an HTML in-app message, or displayed as media in an In-App Automation. Defaults to any Airship-originated URLs and YouTube URLs. - `urlAllowListScopeJavaScriptInterface`: These URLs are checked before the Airship JavaScript interface is injected into the webview. Defaults to any Airship-originated URLs. - `urlAllowList`: Both scopes are applied to these URLs. ```javascript Airship.takeOff({ production: { appKey: "", appSecret: "" }, inProduction: true, site: "us", urlAllowList: ["*"], // Accept all URLs // Or configure specific scopes: urlAllowListScopeOpenUrl: ["https://example.com/*", "https://*.youtube.com/*"], urlAllowListScopeJavaScriptInterface: ["https://example.com/*"] }) ``` **Valid URL pattern syntax** ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` To accept any URL within the SDK, set the `urlAllowList` to `["*"]`. For a complete list of configuration options, see the [AirshipConfig reference](https://www.airship.com/docs/reference/libraries/urbanairship-cordova/latest/interfaces/AirshipConfig.html). ### Push Notifications Configure and implement push notifications for iOS and Android platforms. # Push Notifications > How to configure your application to receive and respond to notifications. ## Platform Setup Before you can send and receive push notifications, you need to configure your app for the platform(s) you're targeting. Follow the platform-specific setup instructions below. ### iOS #### Enable Push Notifications Capability 1. Open your project in Xcode. 2. Click on your project in the Project Navigator. 3. Select your main app target and then click the **Signing & Capabilities** tab. 4. If you do not see Push Notifications enabled, click **+ Capability** and add **Push Notifications**. ![Adding the Push Notifications capability in Xcode](https://www.airship.com/docs/images/ios-enable-push-notifications-capabilities_hu_2e1789fffb02612b.webp) *Adding the Push Notifications capability in Xcode* #### Enable Background Modes 1. Select your main app target and then click the **Signing & Capabilities** tab. 2. Click **+ Capability** and add **Background Modes**. ![Adding the Background Modes capability in Xcode](https://www.airship.com/docs/images/ios-enable-background-mode-capabilities_hu_f135d9fec0ba0d06.webp) *Adding the Background Modes capability in Xcode* 3. In the **Background Modes** section, select the **Remote notifications** checkbox. ![Enabling Remote notifications in Background Modes](https://www.airship.com/docs/images/ios-background-mode-remote-notifications_hu_7e38b08288fcd7b2.webp) *Enabling Remote notifications in Background Modes* #### Signing Add your Apple Developer Account Team ID to the [build.json](https://cordova.apache.org/docs/en/latest/guide/platforms/ios/#using-buildjson). ```json { "ios": { "debug": { "developmentTeam": "XXXXXXXXXX" }, "release": { "developmentTeam": "XXXXXXXXXX" } } } ``` Your iOS builds will need to reference the `build.json` using Cordova's `--buildConfig` flag. #### Notification Service Extension

To take advantage of notification attachments, such as images, animated gifs, and video, you will need to create a notification service extension.

Follow the steps in the [iOS Notification Service Extension Guide](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/). ### Android Configure Firebase Cloud Messaging (FCM) to enable push notifications on Android. #### FCM Setup Add a reference to your `google-services.json` file in the app's `config.xml` and enable Google Services plugin: ```xml ... ``` #### Notification Configuration Configure the notification icon and accent color in your `takeOff` config: ```javascript Airship.takeOff({ production: { appKey: "", appSecret: "" }, development: { appKey: "", appSecret: "" }, inProduction: true, site: "us", android: { notificationConfig: { icon: "ic_notification", accentColor: "#00ff00" } } }) ``` See the [Cordova Plugin Setup guide](https://www.airship.com/docs/developer/sdk-integration/cordova/installation/getting-started/) for complete `takeOff` configuration options. ## Enable User Notifications The Airship SDK distinguishes between *user notifications* (visible to users) and *silent push notifications* (background data delivery). User notifications require explicit permission from the user. By default, user notifications are disabled. Enable them when you want to show visible notifications to users. ### Basic Enablement The simplest way to enable user notifications is with `setUserNotificationsEnabled()`: ```javascript Airship.push.setUserNotificationsEnabled(true) ``` This will prompt the user for permission if not already granted. However, it does not provide feedback on whether the user accepted or denied the permission. > **Note:** For apps that target Android 13 (API 33) and above, enabling user notifications will display a runtime permission prompt. > > To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately on app startup, and instead wait for a more appropriate time to prompt for notification permission. ### Async Enablement For more control over the permission flow, use `enableUserNotifications()` which returns the permission result: ```javascript Airship.push.enableUserNotifications((granted) => { if (granted) { console.log('Notifications enabled') } else { console.log('Notifications denied') } }) ``` ### Checking Notification Status To check if user notifications are currently enabled: ```javascript Airship.push.isUserNotificationsEnabled((enabled) => { console.log('User notifications enabled:', enabled) }) ``` For more detailed status information, use `getNotificationStatus()`: ```javascript Airship.push.getNotificationStatus((status) => { console.log('Are notifications allowed:', status.areNotificationsAllowed) console.log('Is opted in:', status.isOptedIn) }) ``` To monitor notification status changes in real-time: ```javascript const subscription = Airship.push.onNotificationStatusChanged((event) => { console.log('Notification status changed:', event.status) console.log('Is opted in:', event.status.isOptedIn) }) // Later, to cancel the subscription subscription.cancel() ``` ### Getting the Push Token To get the platform-specific push token (APNs token on iOS, FCM token on Android): ```javascript Airship.push.getPushToken((token) => { console.log('Push token:', token) }) // Or listen for token updates const subscription = Airship.push.onPushTokenReceived((event) => { console.log('Push token:', event.pushToken) }) ``` If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/cordova/troubleshooting/push-notifications/) to check notification status and fix common issues. # Notification Events > How to handle push notification events, respond to user interactions, and manage active notifications. The Airship SDK provides event listeners for when a push is received or a notification is interacted with. ## Push Received Listen for when a push notification is received: ```javascript const subscription = Airship.push.onPushReceived((event) => { console.log('Push received:', event.pushPayload) console.log('Is foreground:', event.isForeground) }) // Later, to cancel the subscription subscription.cancel() ``` This event fires when a push notification arrives. On iOS, it fires regardless of whether the app is in the foreground or background. On Android, this event only fires in the foreground. ## Notification Response Listen for when a user interacts with a notification: ```javascript const subscription = Airship.push.onNotificationResponse((event) => { console.log('Notification tapped:', event) console.log('Action ID:', event.actionId) console.log('Is foreground action:', event.isForeground) if (event.actionId === 'custom_action') { // Handle custom action } }) // Later, to cancel the subscription subscription.cancel() ``` This event fires when a user taps on a notification or a notification action button. ## Managing Active Notifications You can retrieve and clear notifications that are currently displayed in the notification center. ### Get Active Notifications Retrieve the list of currently displayed notifications: ```javascript Airship.push.getActiveNotifications((notifications) => { console.log('Active notifications:', notifications) }) ``` > **Note:** On Android, this list only includes notifications sent through Airship. ### Clear Notifications Clear all notifications for the app: ```javascript Airship.push.clearNotifications() ``` Clear a specific notification by identifier: ```javascript Airship.push.clearNotification(identifier) ``` > **Note:** On Android, you can use this method to clear notifications outside of Airship. The identifier is in the format `:`. # Customize Notifications > How to customize push notification presentation, badges, quiet time, and foreground display behavior. ## iOS Notification Options By default, the Airship SDK will request `alert`, `badge`, and `sound` notification options for remote notifications. This can be configured by setting notification options before enabling user notifications. ```javascript Airship.push.iOS.setNotificationOptions([ "alert", "badge", "sound" ]) ``` ### Provisional Authorization Apps can request provisional authorization along with the usual notification options. When requesting provisional authorization, apps do not need to prompt the user for permission initially, and notifications will be delivered in a non-interruptive manner to the Notification Center until the user explicitly chooses to keep delivering messages either prominently or quietly. ```javascript Airship.push.iOS.setNotificationOptions([ "alert", "badge", "sound", "provisional" ]) ``` ### Foreground Presentation Options When a push is received in the foreground on iOS, how the notification is displayed to the user is controlled by foreground presentation options. By default, the SDK will not set any options so the notification will be silenced. ```javascript Airship.push.ios.setForegroundPresentationOptions([ "banner", "list", "sound" ]) ``` ### Badges The badge on iOS presents a counter on top of the application icon. You can control this directly through Airship. ```javascript // Set badge number Airship.push.ios.setBadgeNumber(20) // Reset badge Airship.push.ios.resetBadge() // Enable auto-badge Airship.push.ios.setAutobadgeEnabled(true) ``` > **Important:** When using auto-badge, only modify the badge value through Airship methods to ensure the value stays in sync. ### Quiet Time Quiet time allows you to suppress notifications during specific hours. Notifications are still received but won't be displayed to the user during the quiet time window. ```javascript // Set quiet time hours Airship.push.ios.setQuietTime({ startHour: 22, startMinute: 0, endHour: 7, endMinute: 0 }) // Enable quiet time Airship.push.ios.setQuietTimeEnabled(true) ``` > **Note:** Quiet time is only supported on iOS. ## Android Foreground Notifications By default, push notifications received while the app is in the foreground on Android are displayed to the user. You can control this behavior globally: ```javascript // Disable foreground notifications Airship.push.android.setForegroundNotificationsEnabled(false) // Enable foreground notifications Airship.push.android.setForegroundNotificationsEnabled(true) ``` ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. For Android, all push messages are delivered in the background, but default Airship will treat messages without an `alert` as silent. For iOS, set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). > **Note:** Pushes sent with the `content_available` property (iOS) or without an `alert` (Android) do not have guaranteed delivery. Factors affecting delivery include battery life, whether the device is connected to WiFi, and the number of silent pushes sent within a recent time period. These metrics are determined solely by iOS/Android and APNs/FCM. Therefore, this feature is best used for supplementing the regular behavior of the app rather than providing critical functionality. For instance, an app could use a silent push to pre-fetch new data ahead of time in order to reduce load times when the app is later launched by the user. ### In-App Experiences Configure and control In-App Experiences in Cordova applications. # In-App Experiences > Pause, resume, and control display timing for In-App Experiences. In-App Experiences are automatically enabled when you integrate the Airship SDK. Use these methods to control when and how they are displayed. ## Pausing and Resuming Display You can pause and resume In-App Experiences to control when they are displayed to users. ```javascript // Pause in-app experiences Airship.inApp.setPaused(true) // Resume in-app experiences Airship.inApp.setPaused(false) // Check if paused Airship.inApp.isPaused((isPaused) => { console.log('Is paused:', isPaused) }) ``` ### Auto-Pause on Launch You can configure the SDK to automatically pause In-App Experiences on launch. This is useful if you want to defer showing In-App Experiences until after onboarding or other critical app flows. ```javascript Airship.takeOff({ production: { appKey: "", appSecret: "" }, development: { appKey: "", appSecret: "" }, inProduction: true, site: "us", autoPauseInAppAutomationOnLaunch: true }) ``` See the [Cordova Plugin Setup guide](https://www.airship.com/docs/developer/sdk-integration/cordova/installation/getting-started/) for complete `takeOff` configuration options. When you're ready to display In-App Experiences, call `setPaused(false)`: ```javascript Airship.inApp.setPaused(false) ``` ## Display Interval Control the minimum time between In-App Experience displays to avoid overwhelming users. ```javascript // Set display interval to 5 seconds Airship.inApp.setDisplayInterval(5000) // Get current display interval Airship.inApp.getDisplayInterval((interval) => { console.log('Display interval:', interval) }) ``` The display interval is the minimum time (in milliseconds) that must pass between displaying In-App Experiences. # Custom Views > Register custom native views to use within Scenes. A *Custom View* is a native view from your mobile or web application embedded into a Scene. Custom Views can display any native content your app exposes, so you can reuse that existing content within any screen in a Scene. Custom Views allow you to embed native iOS and Android views within Scenes, giving you full control over design and layout while leveraging Airship's targeting and orchestration capabilities. ## Requirements To use Custom Views in Cordova, you must extend the native Airship modules by implementing native code on each platform. ## Registering Custom Views Custom Views must be registered on each native platform separately: ### iOS See the [Apple Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/custom-views/) for detailed implementation instructions. Custom Views should be registered after takeOff in your native iOS code. You can add this to your `AppDelegate.swift` or a custom plugin: ```swift import AirshipKit // In your AppDelegate or after Airship.takeOff AirshipCustomViewManager.shared.register(name: "my-custom-view") { args in // Return your SwiftUI view MyCustomView(args: args) } ``` ### Android See the [Android Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/custom-views/) for detailed implementation instructions. Custom Views should be registered during the `onAirshipReady` callback in your native Android code. You can use Airship's `Autopilot` to register views at startup: ```kotlin import com.urbanairship.AirshipCustomViewManager import com.urbanairship.Autopilot import com.urbanairship.UAirship class CustomAutopilot : Autopilot() { override fun onAirshipReady(airship: UAirship) { // Register custom views AirshipCustomViewManager.register("my-custom-view") { context, args -> // Return your Android View MyCustomView(context, args) } } } ``` Don't forget to register your `Autopilot` in `AndroidManifest.xml`: ```xml ``` ## Using Custom Views Once registered, Custom Views can be added to Scenes in the Airship dashboard: 1. Create or edit a Scene 2. Add the **Custom View** content element to a screen 3. Enter the view name (e.g., `my-custom-view`) that matches the name you registered in your native code 4. Optionally add key-value pairs to pass custom properties to the view The native view will be displayed within the Scene with the properties you configured. ### Message Center Implement Message Center to provide an inbox for rich HTML-based messages. # Message Center > The default Message Center is available for Cordova with minimal integration required. Basic theming options are supported. Message Center provides an inbox for rich, HTML-based messages. Learn more about Message Center in our [feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Display the Message Center Display the Message Center with a single method call: ```js Airship.messageCenter.display(); ``` To build a custom message list, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/cordova/message-center/embedding/). Individual messages will still display as a native overlay. ## Fetch Messages Retrieve messages from the inbox: ```js Airship.messageCenter.getMessages((messages) => { console.log('Inbox messages: ' + messages); }); ``` ## Listen for Message Updates Subscribe to message updates using event listeners: ```js Airship.addListener('messageCenterUpdated', () => { Airship.messageCenter.getMessages((messages) => { // Handle messages }); }); ``` ## Listen for Unread Count Changes Subscribe to unread count updates: ```js Airship.messageCenter.getUnreadCount((unreadCount) => { // Update badge or UI }); ``` ## Refresh Messages Manually refresh the message list from the server: ```js Airship.messageCenter.refreshMessages( () => { console.log('Refreshed'); }, (error) => { console.log('Failed: ' + error); } ); ``` ## Mark Messages as Read Mark one or more messages as read: ```js Airship.messageCenter.markRead("message-id"); ``` ## Delete Messages Delete one or more messages: ```js Airship.messageCenter.deleteMessage("message-id"); ``` # Embed the Message Center > Create custom Message Center lists with full control over design and navigation. This guide covers creating custom Message Center list implementations for Cordova applications. You can build a custom message list using web technologies, but individual messages must be displayed using the native overlay. > **Note:** Cordova does not support embedding native message views. Use `showMessageView()` to display individual messages as an overlay. ## Override Default Display Behavior To use a custom Message Center list instead of the default UI, disable auto-launch and add a listener to handle display events: ```js // Disable the default UI Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false); // Add a listener to handle display events Airship.messageCenter.onDisplay((event) => { if (event.messageId) { // Show specific message in native overlay Airship.messageCenter.showMessageView(event.messageId); } else { // Navigate to your custom message list navigateToCustomMessageList(); } }); ``` ## Displaying Individual Messages Use `showMessageView` to display messages in a native overlay: ```js Airship.messageCenter.showMessageView("message-id"); ``` ### Preference Center Implement Preference Center to let users control their subscription preferences. # Preference Center > Preference Center allows users to opt in and out of subscription lists configured via the Airship Dashboard. > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. Preference Center provides a pre-built UI for users to manage their subscription preferences. Learn more in the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Display a Preference Center Display a Preference Center with a single method call: ```js Airship.preferenceCenter.display("preference-center-id"); ``` To build a custom Preference Center UI, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/cordova/preference-center/embedding/). # Embed the Preference Center > Create custom Preference Center UIs by fetching the config and building your own subscription management interface. This guide covers creating custom Preference Center UIs for Cordova applications. Unlike the default Preference Center, you'll build your own UI from scratch using the Preference Center configuration and subscription list APIs. ## Override Default Display Behavior To use a custom Preference Center instead of the default UI, disable auto-launch for the specific Preference Center ID and handle display events: ```js // Disable the OOTB UI for this Preference Center Airship.preferenceCenter.setAutoLaunchDefaultPreferenceCenter( "preference-center-id", false ); // Add a listener to handle display events Airship.preferenceCenter.onDisplay((event) => { const preferenceCenterId = event.preferenceCenterId; // Navigate to your custom preference center UI navigateToCustomPreferenceCenter(preferenceCenterId); }); ``` ## Fetching Preference Center Config The Preference Center config contains all the information needed to build your UI, including subscription lists, sections, and display settings. ```js Airship.preferenceCenter.getConfig("preference-center-id", (config) => { // Use config to build your UI }); ``` > **Note:** The config might not be available immediately on first app start. Implement exponential backoff if automatically retrying, or provide a UI for users to manually retry. ## Building Your Custom UI You'll need to: 1. **Fetch the config** to get the list of subscription lists and their current state 2. **Build your UI** using the config data (sections, subscription lists, display settings) 3. **Update subscription lists** when users make changes using the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/cordova/audience/subscription-lists/) ### Example Implementation ```js function loadPreferenceCenter(preferenceCenterId) { Airship.preferenceCenter.getConfig(preferenceCenterId, (config) => { // Build your UI with the config const container = document.getElementById('preference-center'); // Display title const title = document.createElement('h1'); title.textContent = config.display.name; container.appendChild(title); // Display sections config.sections.forEach((section) => { const sectionDiv = document.createElement('div'); const sectionTitle = document.createElement('h2'); sectionTitle.textContent = section.display.name; sectionDiv.appendChild(sectionTitle); // Display items (subscription lists) section.items.forEach((item) => { const itemDiv = document.createElement('div'); const label = document.createElement('label'); label.textContent = item.display.name; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = isSubscribed(item.subscriptionId); checkbox.addEventListener('change', (e) => { if (e.target.checked) { Airship.contact.subscriptionLists.subscribe(item.subscriptionId); } else { Airship.contact.subscriptionLists.unsubscribe(item.subscriptionId); } }); itemDiv.appendChild(checkbox); itemDiv.appendChild(label); sectionDiv.appendChild(itemDiv); }); container.appendChild(sectionDiv); }); }); } ``` > **Important:** Preference Center configuration is currently limited to subscription lists only. Use the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/cordova/audience/subscription-lists/) to manage user subscriptions. ### Audience Management Integrate audience management features into your Cordova app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs and listen for channel creation. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. ```js // Get the channel ID (may return null if not yet created) Airship.channel.getChannelId((channelID) => { console.log("Channel: " + channelID) }) // Wait for the channel ID to be created (returns the channel ID once available) Airship.channel.waitForChannelId((channelID) => { console.log("Channel: " + channelID) }) ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Use `waitForChannelId()` if you need to wait for the channel to be created before proceeding. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. ```js Airship.channel.onChannelCreated((event) => { console.log('Channel created: ' + event.channelId); }); ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring the takeOff options, see [Cordova SDK Setup](https://www.airship.com/docs/developer/sdk-integration/cordova/installation/getting-started/). ```js Airship.takeoff({ ... isChannelCaptureEnabled: false, }) ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during takeOff. For more information about Privacy Manager, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. ```js Airship.contact.identify(namedUserId); ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. ```js Airship.contact.reset(); ``` You can get the Named User ID only if you set it through the SDK. ```js Airship.contact.getNamedUserId((namedUser) => { if (namedUser) { console.log("Named User ID: " + namedUser) } }); ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. ```js Airship.channel.editTags() .addTags(["one", "two", "three"]) .removeTags(["some_tag"]) .apply() // Accessing channel tags Airship.channel.getTags((tags) => { console.log("Tags: " + tags) }); ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```js Airship.channel.editTagGroups() .addTags("loyalty", ["silver-member"]) .removeTags("loyalty", ["bronze-member"]) .apply() ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```js Airship.contact.editTagGroups() .addTags("loyalty", ["silver-member"]) .removeTags("loyalty", ["bronze-member"]) .apply() ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. ```js Airship.channel.editAttributes() .setAttribute("device_name", "Bobby's Phone") .setAttribute("average_rating", 4.99) .removeAttribute("vip_status") .apply() ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. ```js Airship.contact.editAttributes() .setAttribute("first_name", "Bobby") .apply() ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. ```js Airship.contact.editAttributes() .setJsonAttribute("attribute_name", "instance_id", {"key":"value", "another_key":"another_value"}) .removeJsonAttribute("some_attribute_name", "some_instance_id") .apply() ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. ```js // Modifying channel subscription lists Airship.channel.editSubscriptionLists() .subscribe("food") .unsubscribe("sports") .apply() // Fetching channel subscription lists Airship.channel.getSubscriptionLists((lists) => { console.log("channel subscriptions: " + lists) }); ``` ## Contact Subscription Lists Contact subscriptions are set at the user-level and require a Channel scope specifying the types that the subscription list applies to. ```js // Modifying contact subscription lists Airship.contact.editSubscriptionLists() .subscribe("food", "app") .unsubscribe("sports", "sms") .apply() // Fetching contact subscription lists Airship.contact.getSubscriptionLists((lists) => { console.log("contact subscriptions: " + lists) }); ``` ## Verifying Subscription Lists To verify that subscription lists have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ### Data Collection Overview of data collection and controls provided by the Airship Cordova SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: | Privacy Manager Flag | Features | |----------------------|----------| | `push` | Push notifications | | `in_app_automation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | | `message_center` | Message Center | | `tags_and_attributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | | `contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | | `analytics` | Associated identifiers, Custom events, Screen tracking, Surveys, email address, Feature Flag interaction | | `feature_flags` | Feature Flag evaluation and interaction | | `all` | All features | | `none` | No features | ## Configuring default enabled features You can configure which features are enabled by default when the SDK initializes. This is done in your Airship config during `takeOff`. For information about setting up the Airship SDK and configuring the takeOff options, see [Cordova SDK Setup](https://www.airship.com/docs/developer/sdk-integration/cordova/installation/getting-started/). ### Default configuration (all features enabled) By default, all features are enabled. The SDK will collect data and make network requests as configured. ### Disabling all features To disable all features by default (useful for consent opt-in flows), set the enabled features to an empty array: ```javascript Airship.takeOff({ default: { enabledFeatures: [] }, ... }); ``` ### Enabling specific features To enable only specific features by default: ```javascript Airship.takeOff({ default: { enabledFeatures: ["push", "analytics"] }, ... }); ``` ## Modifying enabled features at runtime You can enable or disable features at any time after takeOff. Once you modify the enabled features from the default, those settings are persisted between app launches. ### Enabling features ```javascript Airship.privacyManager.enableFeatures(["push", "analytics"]); ``` ### Disabling features ```javascript Airship.privacyManager.disableFeatures(["push", "analytics"]); ``` ## Consent opt-in flow example A common use case is to start with all features disabled, then enable them as users grant consent: ```javascript // Start with all features disabled Airship.takeOff({ default: { enabledFeatures: [] }, ... }); // Later, when user grants consent: function userGrantedConsent() { // Enable features based on user's consent choices Airship.privacyManager.enableFeatures(["push", "analytics"]); } ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Apple Privacy Manifest](https://www.airship.com/docs/reference/data-collection/apple-privacy-manifest/) - Declare data collection practices to Apple (iOS) - [Google Play Data Safety](https://www.airship.com/docs/reference/data-collection/google-play-data-safety/) - Reference for Google Play's Data Safety section (Android) - [Analytics](https://www.airship.com/docs/developer/sdk-integration/cordova/data-collection/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/cordova/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with custom events. They require enabling analytics for your app. For detailed information, see the [Custom Events guide](https://www.airship.com/docs/guides/audience/events/custom-events/). ```javascript let event = { eventName: "event_name", eventValue: 123.12, properties: { "my_custom_property": "some custom value", "is_neat": true, "any_json": { "foo": "bar" } } } Airship.analytics.addCustomEvent(event) ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. ```javascript Airship.analytics.setAssociatedIdentifier("key", "value") ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. ```javascript Airship.analytics.trackScreen("MainScreen") ``` ### Troubleshooting Common issues and solutions for Airship plugin setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/cordova/installation/getting-started/) or [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/cordova/installation/advanced-configuration/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Installation Errors If you encounter errors during installation: - Verify that you're using a compatible version of Cordova. - Ensure the plugin is properly installed with `cordova plugin add`. - Check that your iOS and Android projects are correctly configured. ## Initialization Errors If you encounter errors during SDK initialization: - Verify your credentials in the Airship dashboard. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. - Check that `takeOff` is called correctly in your app. - Ensure both iOS and Android native modules are properly installed. # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/cordova/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. ## Push Notifications Not Working If push notifications are not being received: - Verify that push notifications are enabled for both iOS and Android. - Check that APNs (iOS) and FCM (Android) are properly configured. - Ensure the app has notification permissions. ## Checking Push Notification Status If you're having trouble with push notifications, check the notification status to see the current state: ```javascript Airship.push.getNotificationStatus((status) => { console.log('Are notifications allowed:', status.areNotificationsAllowed) console.log('Is opted in:', status.isOptedIn) console.log('Is user notifications enabled:', status.isUserNotificationsEnabled) console.log('Is user opted in:', status.isUserOptedIn) console.log('Is pushable:', status.isPushTokenRegistered) console.log('Is push privacy feature enabled:', status.isPushPrivacyFeatureEnabled) }) ``` You can also listen for status changes to debug permission issues: ```javascript const subscription = Airship.push.onNotificationStatusChanged((event) => { console.log('Notification status changed:', event.status) if (!event.status.areNotificationsAllowed) { console.log('System-level notifications are disabled') } if (!event.status.isUserNotificationsEnabled) { console.log('User notifications are disabled in Airship') } if (!event.status.isPushTokenRegistered) { console.log('Push token not registered') } }) // Later, to cancel the subscription subscription.cancel() ``` ## Flutter Integrate the Airship SDK into your Flutter applications for iOS and Android. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Message Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/flutter/message-center/getting-started/) and [Preference Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/flutter/preference-center/getting-started/). ```dart Airship.onDeepLink.listen((event) { var deepLink = event.deepLink; // Handle deep link }); ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Running Actions You can run actions programmatically using the `Airship.actions.run()` method. The method returns a `Future` that resolves with the action result. The action value can be a string, number, boolean, null, object, or array. **Running an action** ```dart // Run an action with a string value using async/await try { String? result = await Airship.actions.run("action_name", "action_value"); print("Action result: $result"); } catch (error) { print("Action error: $error"); } // Run an action with a Map value try { String? result = await Airship.actions.run("action_name", { "key": "value", "number": 42 }); print("Action result: $result"); } catch (error) { print("Action error: $error"); } // Run an action without a value try { String? result = await Airship.actions.run("action_name", null); print("Action result: $result"); } catch (error) { print("Action error: $error"); } // Run an action using Future.then() Airship.actions.run("action_name", "action_value") .then((result) { print("Action result: $result"); }) .catchError((error) { print("Action error: $error"); }); ``` ## Custom Actions You can register custom actions with an [Airship extender](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/extending-airship/) and with native SDK APIs. See the native platform documentation for details: - [iOS Actions](https://www.airship.com/docs/developer/sdk-integration/apple/actions/) - [Android Actions](https://www.airship.com/docs/developer/sdk-integration/android/actions/) # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. ```dart var flag = await Airship.featureFlagManager.flag("my-flag"); if (flag.isEligible) { // Do something with the flag } else { // Disable feature or use default behavior } ``` ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/flutter/data-collection/privacy-manager/). ```dart Airship.featureFlagManager.trackInteraction(flag) ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. ```dart Airship.featureFlagManager.flag("another_rad_flag").then((flag) => { if (flag.isEligible) { // Do something with the flag } }).catchError((error) => { debugPrint("flag error: $error") }); ``` # Live Activities > Integrate Live Activities into your Flutter app to display real-time updates on the iOS Lock Screen and Dynamic Island. {{< badge "axp" >}} For the push API method, see the [iOS Live Activities](https://www.airship.com/docs/guides/messaging/features/ios-live-activities/) messaging guide. See also the [iOS Live Activities](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ### App setup Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/#extending-airship), make a call to `LiveActivityManager.shared.setup` to configure any Live Activities for the app. Call `configurator.register` for each Live Activity type that your application defines and include a block on how to parse the name of the activity that you will use to track on Airship. This name will be used to send updates through APNS. ```swift import Foundation import AirshipKit import AirshipFrameworkProxy import ActivityKit // This class header is required to be automatically picked up by the Airship plugin: @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { public static func onAirshipReady() { if #available(iOS 16.1, *) { // Will throw if called more than once try? LiveActivityManager.shared.setup { configurator in // Call for each Live Activity type await configurator.register(forType: Activity.self) { attributes in // Track this property as the Airship name for updates attributes.gameID } } } // other setup } } ``` ### Starting Live Activities For any Live Activities configured, you can start a new one using the `start` method: ```dart if (Platform.isIOS) { LiveActivityStartRequest startRequest = LiveActivityStartRequest( attributesType: 'SportsActivityAttributes', attributes: { "gameID": "sports-game-123", }, content: LiveActivityContent(status: 'Game Pending', relevanceScore: 0.0)); await Airship.liveActivityManager.start(startRequest); } ``` ### Updating Live Activities To update, use `update`, but you will need the activity ID. ```dart if (Platform.isIOS) { List activities = await Airship.liveActivityManager.listAll(); LiveActivity? activity = activities .where((activity) => activity.attributes.gameID == 'sports-game-123') .firstOrNull; if (activity != null) { LiveActivityContent content = LiveActivityContent( state: {'status': 'Game starting!'}, relevanceScore: 0.0, ); LiveActivityUpdateRequest updateRequest = LiveActivityUpdateRequest( attributesType: 'SportsGameAttributes', activityId: activity.id, content: content, ); await Airship.liveActivityManager.update(updateRequest); } } ``` ### Ending Live Activities To end is similar to `update`. Use `end` with the activity ID: ```dart if (Platform.isIOS) { List activities = await Airship.liveActivityManager.listAll(); LiveActivity? activity = activities .where((activity) => activity.attributes.gameID == 'sports-game-123') .firstOrNull; if (activity != null) { LiveActivityStopRequest stopRequest = LiveActivityStopRequest( attributesType: 'SportsGameAttributes', activityId: activity.id, dismissalPolicy: LiveActivityDismissalPolicyDefault(), ); await Airship.liveActivityManager.end(stopRequest); } } ``` # Live Updates > Integrate Live Updates into your Flutter app to update content in real-time without requiring an app update. {{< badge "axp" >}} For the push API method, see the [Android Live Updates](https://www.airship.com/docs/guides/messaging/features/android-live-updates/) messaging guide. See also the [Android Live Updates](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ### Creating a handler The Airship SDK supports two types of Live Update handlers: * `NotificationLiveUpdateHandler` — Displays a notification with a custom layout, with content updated by the Live Update. * `CustomLiveUpdateHandler` — Receives Live Update events and provides flexibility to display content using a custom implementation. This can be used to power home screen widgets, views embedded in the app, and more. Each handler type has two different interfaces that may be implemented, to support suspending or callback-based code: * `SuspendLiveUpdateNotificationHandler` * `CallbackLiveUpdateNotificationHandler` * `SuspendLiveUpdateCustomHandler` * `CallbackLiveUpdateCustomHandler` The following `SampleLiveUpdateHandler` reads content from the Live Update payload and displays scores for a sports game in a custom notification layout, using `RemoteViews`: ```kotlin class SampleLiveUpdateHandler : SuspendLiveUpdateNotificationHandler() { override suspend fun onUpdate( context: Context, event: LiveUpdateEvent, update: LiveUpdate ): LiveUpdateResult { // Read content_state fields from the Live Update payload val teamOneScore = update.content.opt("team_one_score").getInt(0).toString() val teamTwoScore = update.content.opt("team_two_score").getInt(0).toString() val statusUpdate = update.content.opt("status_update").optString() // Expanded notification layout val bigLayout = RemoteViews(context.packageName, R.layout.sports_big).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) setTextViewText(R.id.statusUpdate, statusUpdate) } // Collapsed notification layout val smallLayout = RemoteViews(context.packageName, R.layout.sports_small).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) } // Create the notification builder val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_EVENT) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(smallLayout) .setCustomBigContentView(bigLayout) // Return 'ok' with the notification builder. // The Airship SDK will handle posting the notification. // Returning LiveUpdateResult.cancel() will end the Live Update and dismiss the notification. return LiveUpdateResult.ok(builder) } companion object { private const val NOTIFICATION_CHANNEL_ID = "sports" } } ``` ### Registering a handler Handlers must be registered with `LiveUpdateManager` in order to receive Live Update events. This should be done *once* after `takeOff`. Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/#extending-airship), register the types. ```kotlin @Keep public final class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { LiveUpdateManager.shared().run { register(type = "notification", handler = SampleLiveUpdateHandler()) } } } ``` > **Note:** The `type` used above, `"notification"`, is used to map Live Update events to the corresponding handler in your app. > The value can be any string that is unique across all handlers registered by an app. This also allows a single handler to manage multiple Live Updates that each have a unique `name`. ### Starting Live Updates Live Updates can be started from within the app. ```dart if (Platform.isAndroid) { LiveUpdateStartRequest createRequest = LiveUpdateStartRequest( name: "sports-game-123", type: 'notification', content: { 'team_one_score': 0, 'team_two_score': 0, 'status_update': 'Game started!' } ); await Airship.liveUpdateManager.start(createRequest); } ``` ### Updating Live Updates Live Updates can be updated from within the app. ```dart if (Platform.isAndroid) { List updates = await Airship.liveUpdateManager.listAll(); LiveUpdateUpdateRequest request = LiveUpdateUpdateRequest( name: "sports-game-123", content: { 'team_one_score': 0, 'team_two_score': 0, 'status_update': 'Game started!' } ); await Airship.liveUpdateManager.update(request); } ``` ### Ending Live Updates You can end a Live Update from within the app. ```dart if (Platform.isAndroid) { List updates = await Airship.liveUpdateManager.listAll(); LiveUpdateEndRequest stopRequest = LiveUpdateEndRequest( name: "sports-game-123" ); await Airship.liveUpdateManager.end(stopRequest); } ``` ### Clearing all active Live Updates During development, it can be useful to reset Live Update tracking on app launch. This allows any Live Updates to be started fresh, even if they were already started during a previous launch. To end all currently active Live Updates, call the `clearAll()` method on `LiveUpdateManager`. ```dart if (Platform.isAndroid) { await Airship.liveUpdateManager.clearAll(); } ``` # Flutter Plugin Changelog > The latest updates to the Airship Flutter plugin. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 12.0.0 May 12, 2026 Major release that raises the minimum supported Flutter version to 3.24.0 to fully support Swift Package Manager on iOS. ### Changes - Raised minimum Flutter version to 3.24.0 and Dart SDK to 3.5.0 - Improved Swift Package Manager support for the iOS plugin ## 11.4.0 May 2, 2026 Minor release that updates the Android SDK to 20.7.0 and the iOS SDK to 20.7.0. ### Changes - Updated Android SDK to [20.7.0](https://github.com/urbanairship/android-library/releases/tag/20.7.0) - Updated iOS SDK to [20.7.0](https://github.com/urbanairship/ios-library/releases/tag/20.7.0) ## 11.3.1 April 9, 2026 Patch release that fixes iOS cold start push notification and deep link events not firing. ### Changes - Fixed iOS cold start push notification and deep link events not firing due to the plugin loader initializing too late to set `UNUserNotificationCenter.delegate` ## 11.3.0 April 1, 2026 Minor release that updates the Android SDK to 20.6.1 and the iOS SDK to 20.6.0 ### Changes - Updated Android SDK to [20.6.1](https://github.com/urbanairship/android-library/releases/tag/20.6.1) - Updated iOS SDK to [20.6.0](https://github.com/urbanairship/ios-library/releases/tag/20.6.0) ## 11.2.0 March 19, 2026 Minor release that updates the Android SDK to 20.5.0 and the iOS SDK to 20.5.0. ### Changes - Updated Android SDK to [20.5.0](https://github.com/urbanairship/android-library/releases/tag/20.5.0) - Updated iOS SDK to [20.5.0](https://github.com/urbanairship/ios-library/releases/tag/20.5.0) ## 11.1.0 January 23, 2026 Minor release that includes accessibility improvements for Message Center and fixes a potential crash on Android. ### Changes - Updated Android SDK to [20.1.1](https://github.com/urbanairship/android-library/releases/tag/20.1.1) - Updated iOS SDK to [20.1.1](https://github.com/urbanairship/ios-library/releases/tag/20.1.1) - Fixed a potential crash in Android Scenes with specific image and display settings. - Improved VoiceOver focus handling for Message Center on iOS. - Fixed an issue where the Message Center title was not being marked as a heading on Android. ## 11.0.0 January 14, 2026 Major release that updates the Android SDK to 20.0.4 and iOS SDK to 20.0.2. ### Changes - Updated Android SDK to [20.0.6](https://github.com/urbanairship/android-library/releases/tag/20.0.6) - Updated iOS SDK to [20.0.3](https://github.com/urbanairship/ios-library/releases/tag/20.0.3) - Updated Kotlin to 2.0.21 - Minimum iOS deployment target is now iOS 16.0 (requires Xcode 14+) - The `AirshipPluginExtender.onAirshipReady` method no longer receives a `UAirship` instance. Use the static `Airship` accessor instead. ## 10.10.1 November 15, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Updated Android SDK to [19.13.6](https://github.com/urbanairship/android-library/releases/tag/19.13.6) - Updated iOS SDK to [19.11.2](https://github.com/urbanairship/ios-library/releases/tag/19.11.2) ## 10.10.0 November 3, 2025 Minor release that updates the Android SDK to 19.13.5 and the iOS SDK to 19.11.1 ### Changes - Updated Android SDK to [19.13.5](https://github.com/urbanairship/android-library/releases/tag/19.13.5) - Updated iOS SDK to [19.11.1](https://github.com/urbanairship/ios-library/releases/tag/19.11.1) ## 10.9.0 October 29, 2025 Minor release that updates the Android SDK to 19.13.4 and the iOS SDK to 19.11.0 ### Changes - Updated Android SDK to [19.13.4](https://github.com/urbanairship/android-library/releases/tag/19.13.4) - Updated iOS SDK to [19.11.0](https://github.com/urbanairship/ios-library/releases/tag/19.11.0) - Fixed an issue where the app would hang when offline due to improper exception handling in feature flags. ## 10.8.0 August 27, 2025 Minor release that updates the Android SDK to 19.11.0 and the iOS SDK to 19.8.3. ### - Updated Android SDK to [19.11.0](https://github.com/urbanairship/android-library/releases/tag/19.11.0) - Updated iOS SDK to [19.8.3](https://github.com/urbanairship/ios-library/releases/tag/19.8.3) - Updated Android event emitting to wait for an attached activity for all events but background push recieved. - Fixed Feature Flag method bindings for `Airship.featureFlagManager.trackInteraction` and `Airship.featureFlagManager.setFlagInResultCache` on Android. ## 10.7.1 August 21, 2025 Patch release with several bug fixes for Scenes, including an important reporting fix for embedded content. ### Changes - Updated Android SDK to [19.10.2](https://github.com/urbanairship/android-library/releases/tag/19.10.2) - Updated iOS SDK to [19.8.2](https://github.com/urbanairship/ios-library/releases/tag/19.8.2) ## 10.7.0 August 1, 2025 Minor release that updates the Android SDK to 19.10.0 and the iOS SDK to 19.8.0 ### Changes - Updated Android SDK to [19.10.0](https://github.com/urbanairship/android-library/releases/tag/19.10.0) - Updated iOS SDK to [19.8.0](https://github.com/urbanairship/ios-library/releases/tag/19.8.0) ## 10.6.0 July 24, 2025 Minor release that fixes Flutter methods calls to avoid crashes when the native side throws an error. Apps using Feature flags are encouraged to update. ### Changes - Fixed crash for Feature Flags methods ## 10.5.0 June 26, 2025 Minor release that updates the Android SDK to 19.9.1 and the iOS SDK to 19.6.1 ### Changes - Updated Android SDK to [19.9.1](https://github.com/urbanairship/android-library/releases/tag/19.9.1) - Updated iOS SDK to [19.6.1](https://github.com/urbanairship/ios-library/releases/tag/19.6.1) - Added support for Android `logPrivacyLevel` configuration option in `AndroidConfig` ## 10.4.0 May 16, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. - Updated Android SDK to [19.7.0](https://github.com/urbanairship/android-library/releases/tag/19.7.0) - Updated iOS SDK to [19.4.0](https://github.com/urbanairship/ios-library/releases/tag/19.4.0) ## 10.3.1 May 9, 2025 Patch release that updates the iOS SDK to 19.3.2 ### Changes - Updated iOS SDK to [19.3.2](https://github.com/urbanairship/ios-library/releases/tag/19.3.2) ## 10.3.0 May 5, 2025 Minor release that updates the Android SDK to 19.6.2 and the iOS SDK to 19.3.1 and fixes an Embedded View bug. ### Changes - Updated Android SDK to [19.6.2](https://github.com/urbanairship/android-library/releases/tag/19.6.2) - Updated iOS SDK to [19.3.1](https://github.com/urbanairship/ios-library/releases/tag/19.3.1) - Added support for JSON attributes - Added new method `Airship.channel.waitForChannelId()` that waits for the channel ID to be created - Fixed bug in `Airship.inApp.isEmbeddedAvailableStream` that disrupted gated rendering of Embedded Views ## 10.2.0 March 27, 2025 Minor release that updates the Android SDK to 19.4.0 and the iOS SDK to 19.1.1 ### Changes - Updated Android SDK to [19.1.1](https://github.com/urbanairship/android-library/releases/tag/19.1.1) - Updated iOS SDK to [19.4.0](https://github.com/urbanairship/ios-library/releases/tag/19.4.0) ## 10.1.0 February 12, 2025 Minor release that updates the Android SDK to 19.1.0 and the iOS SDK to 19.0.3 ### Changes - Updated Android SDK to [19.1.0](https://github.com/urbanairship/android-library/releases/tag/19.1.0) - Updated iOS SDK to [19.0.3](https://github.com/urbanairship/ios-library/releases/tag/19.0.3) ## 10.0.0 February 6, 2025 Major release that updates the Android SDK to 19.0.0 and the iOS SDK to 19.0.3 ### Changes - Updated Android SDK to [19.0.0](https://github.com/urbanairship/android-library/releases/tag/19.0.0) - Updated iOS SDK to [19.0.3](https://github.com/urbanairship/ios-library/releases/tag/19.0.3) ## 9.1.1 January 17, 2025 Patch release that updates the Android SDK to 18.6.0 and the iOS SDK to 18.14.2 ### Changes - Updated Android SDK to [18.6.0](https://github.com/urbanairship/android-library/releases/tag/18.6.0) - Updated iOS SDK to [18.14.2](https://github.com/urbanairship/ios-library/releases/tag/18.14.2) ## 9.1.0 December 7, 2024 Minor release that updates the Android Airship SDK to 18.5.0 and iOS Airship SDK to 18.13.0 ### Changes - Updated Android SDK to [18.5.0](https://github.com/urbanairship/android-library/releases/tag/18.5.0). - Updated iOS SDK to [18.13.0](https://github.com/urbanairship/ios-library/releases/tag/18.13.0). ## 9.0.1 November 28, 2024 Patch release that updates the iOS SDK to 18.12.2 and Android SDK to 18.4.2 ### Changes - Updated Android SDK to 18.4.2. - Updated iOS SDK to 18.12.2. ## 9.0.0 November 16, 2024 Major version release that drops support for v1 embeddings. ### Changes - Drops support for deprecated v1 embeddings. ## 8.0.4 November 8, 2024 Patch release that resolves an issue with Firebase integrations and fixes an issue with opt-in checks when requestAuthorizationToUseNotifications is set to false on iOS. ### Changes - Updated Airship iOS SDK to [18.12.1](https://github.com/urbanairship/ios-library/releases/tag/18.12.1) - Fixed issues caused by swizzling conflicts with some Firebase framework integrations. - Fixed opt-in check permissions querying when requestAuthorizationToUseNotifications is set to false on iOS. ## 8.0.3 November 7, 2024 Patch release that resolves an issue with Firebase integrations and fixes an issue with opt-in checks when requestAuthorizationToUseNotifications is set to false on iOS. ### Changes - Updated Airship iOS SDK to [18.12.1](https://github.com/urbanairship/ios-library/releases/tag/18.12.0) - Fixed issues caused by swizzling conflicts with some Firebase framework integrations. - Fixed opt-in check permissions querying when requestAuthorizationToUseNotifications is set to false on iOS. ## 8.0.2 November 4, 2024 ## Version 8.0.2 - November 4, 2024 Patch release that updates to latest SDKs and resolves an issue with Firebase integrations. Applications that integrate with Firebase are encouraged to update. ### Changes - Updated Airship Android SDK to [18.4.0](https://github.com/urbanairship/android-library/releases/tag/18.4.0) - Updated Airship iOS SDK to [18.12.0](https://github.com/urbanairship/ios-library/releases/tag/18.12.0) - Fixed token clearing during registration retries when Firebase is not yet ready. ## 8.0.1 October 25, 2024 Patch release that fixes an issue with event streams that causes deep links to fail when the app is launched from a terminated state. Apps that use deep linking are encouraged to update. ### Changes - Fixed event stream handling for initial events - Fixed tracking live activities started from a push notification ## 8.0.0 October 25, 2024 Major version that makes it easier to include Airship in a hybrid app. The only breaking change is when extending the AirshipPluginExtender protocol on java there is a new extendConfig(Contex, AirshipConfigOptions.Builder) method to implement. Most application will not be affected. ### Changes - Added new methods to the plugin extender to make hybrid app integrations easier ## 7.9.0 October 21, 2024 Minor version release with several new features including: iOS Live Activities, Android Live Updates, Message Center improvements, and iOS notification service extension support in the iOS example project. ### Changes - Updated Airship Android SDK to [18.3.3](https://github.com/urbanairship/android-library/releases/tag/18.3.3) - Updated Airship iOS SDK to [18.11.1](https://github.com/urbanairship/ios-library/releases/tag/18.11.1) - Added `notificationPermissionStatus` to `PushNotificationStatus` - Added options to `enableUserNotifications` to specify the `PromptPermissionFallback` when enabling user notifications - Added new `showMessageCenter(messageId?: string)` and `showMessageView(messageId: string)` to `MessageCenter` to display the OOTB UI even if `autoLaunchDefaultMessageCenter` is disabled - Added new APIs to manage [iOS Live Activities](https://docs.airship.com/platform/mobile/ios-live-activities/) - Added new APIs to manage [Android Live Updates](https://docs.airship.com/platform/mobile/android-live-updates/) - Added a new [iOS plugin extender]() to modify the native Airship SDK after takeOff - Added new [Android plugin extender]() to modify the native Airship SDK after takeOff ## 7.8.2 September 13, 2024 Patch release that fixes a potential Swift compile error. ### Changes - Fixed a potential Swift compile error. ## 7.4.0 September 10, 2024 Minor release that updates the Android SDK to 17.8.1 and iOS SDK to 18.2.2 ### Changes - Updated Android SDK to 17.8.1. - Updated iOS SDK to 18.2.2. ## 7.8.1 September 10, 2024 Patch release that brings back in-app messages methods and fixes a potential Swift compile error. ### Changes - Brought back `setPaused()`, `isPaused()`, `setDisplayInterval()` and `displayInterval()` methods. - Fixed a potential Swift compile error. ## 7.8.0 September 4, 2024 Minor release that adds early access support for Embedded Content. ## Changes - Adds AirshipEmbeddedView and listener methods to Airship.inApp for Embedded Content. ## 7.7.1 August 17, 2024 Patch release that adds a message center message list refresh operation on iOS. This allows message center messages to properly display when launched from a push while the iOS app is backgrounded. iOS apps that open message center messages directly from push notifications are encouraged to update. ### Changes - Refresh message center messages when message is initially unavailable on iOS. ## 7.7.0 August 13, 2024 Minor release that fixes test devices audience check, holdout group experiments displays and in-app experience displays when resuming from a paused state. Apps that use in-app experiences are encouraged to update. ### Changes - Updated Android SDK to 18.1.6. - Updated iOS SDK to 18.7.2. - Fixed test devices audience check. - Fixed holdout group experiments displays. - Fixed in-app experience displays when resuming from a paused state. ## 7.6.0 July 11, 2024 Minor release that updates the Android SDK to 18.1.1 and the iOS SDK to 18.5.0. ### Changes - Updated Android SDK to 18.1.1 - Updated iOS SDK to 18.5.0 - Updated airship-mobile-framework-proxy to 7.0.0 - Updated Kotlin version to 1.9.0 - Added support for configuring log privacy level on iOS ## 7.5.0 June 21, 2024 Minor release that updates iOS SDK to 18.4.1, updates Android compileSDKVersion from 33 to 34, sets Android source and target compatibility to Java 17, updates android example build configuration, improves example UI, and updates the airship mobile framework proxy to 6.3.1 which includes a fix for event management. ### Changes - Updated iOS SDK to 18.4.1 - Updated airship-mobile-framework-proxy to 6.3.1 - Fixed Event Emitter bug - Updated Android compileSDKVersion from 33 to 34 and set source and target compatibility to Java 17 - Updated Android example build configration - Improved example UI [View Older Releases](https://github.com/urbanairship/airship-flutter/releases?q=created%3A%3C2024-05-15&expanded=true) # Flutter Plugin Resources > API documentation, source code, and changelogs for the Airship Flutter plugin. ## Platform Support {#platform-support} | Feature | iOS | Android | |----------------------------------------|-----|---------| | Push Notifications | ✅ | ✅ | | Live Activities | ✅ | ❌ | | Live Updates | ❌ | ✅ | | In-App Experiences | ✅ | ✅ | | Custom Views | ✅ | ✅ | | Embedded Content | ✅ | ✅ | | Message Center | ✅ | ✅ | | Preference Center | ✅ | ✅ | | Feature Flags | ✅ | ✅ | | Analytics | ✅ | ✅ | | Contacts | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ## API References * [Flutter API Documentation](https://www.airship.com/docs/reference/libraries/flutter/latest/) ## GitHub Samples * [Flutter Sample App](https://github.com/urbanairship/airship-flutter/tree/main/example) ## Source * [Source](https://github.com/urbanairship/airship-flutter) ## Changelog * [Flutter Changelog](https://www.airship.com/docs/developer/sdk-integration/flutter/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [Flutter License](https://github.com/urbanairship/airship-flutter/blob/main/LICENSE) ### SDK Installation Install and configure the Airship Flutter plugin for iOS and Android applications. # Install and Set Up the Flutter Plugin > Learn how to install the Airship Flutter plugin, configure iOS and Android platforms, and initialize the SDK. This guide walks you through installing the Airship Flutter plugin and configuring it for both iOS and Android platforms. ## Requirements - Flutter 3.0.2 or higher - iOS 15+ (Xcode 16+) - Android API 23+ ## Install the plugin 1. Add the Airship dependency to your package's `pubspec.yaml` file: ```yaml dependencies: airship_flutter: ^flutterPluginVersion ``` 2. Install your Flutter package dependencies by running the following in the command line at your project's root directory: `flutter pub get` 3. Import Airship into your Dart code: ```dart import 'package:airship_flutter/airship_flutter.dart'; ``` ## Initialize Airship The Airship SDK requires a single entry point called `takeOff` to initialize. Call `takeOff` in your app's `main()` function before running the app. This ensures Airship is ready before any widgets are built. ### Get your app credentials Before calling `takeOff`, you'll need your app credentials from the Airship dashboard: 1. Log in to the [Airship dashboard](https://go.airship.com/) and open your project. 1. Select the dropdown menu (▼) next to your project name, and then **Project details**. 1. Copy your **App Key** and **App Secret**. > **Important:** Airship provides separate credentials for development and production environments. The SDK automatically selects the correct credentials based on your build configuration. However, Flutter doesn't have a built-in way to detect this, so you can use the `inProduction` flag or configure based on build mode. ### Configure takeOff The following example shows how to configure and call `takeOff` in your Flutter app: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; void main() { // Ensure Flutter is initialized WidgetsFlutterBinding.ensureInitialized(); // Configure Airship var config = AirshipConfig( // Development environment credentials defaultEnvironment: ConfigEnvironment( appKey: "YOUR_DEVELOPMENT_APP_KEY", appSecret: "YOUR_DEVELOPMENT_APP_SECRET" ), // Production environment credentials (optional but recommended) productionEnvironment: ConfigEnvironment( appKey: "YOUR_PRODUCTION_APP_KEY", appSecret: "YOUR_PRODUCTION_APP_SECRET" ), // Set to true for production builds inProduction: false, // Set to true for production or use const bool.fromEnvironment('dart.vm.product') // Cloud site: Site.us for US, Site.eu for EU site: Site.us ); // Initialize Airship Airship.takeOff(config); // Run your app runApp(MyApp()); } ``` ### Configuration options Key configuration options include: * **defaultEnvironment**: Credentials for your development/staging environment * **productionEnvironment**: Credentials for your production environment (optional but recommended) * **inProduction**: Set to `true` for production builds, `false` for development. You can use `const bool.fromEnvironment('dart.vm.product')` to automatically detect release builds * **site**: Set to `Site.us` for US cloud projects or `Site.eu` for EU cloud projects For additional configuration options (such as URL allowlists), see [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/advanced-configuration/). > **Note:** Once `takeOff` is called, the config is stored and applied for future app sessions. If you call `takeOff` again with a different config, the new config will not take effect until the next app restart. ## Test the integration After completing the setup, verify your integration to ensure everything is working correctly. 1. **Build and run your app** on both iOS and Android (if applicable): `flutter run` 2. **Check the console logs** for Airship initialization: * Look for log messages indicating successful SDK initialization * You should see a **Channel ID** in the logs—this is the unique identifier for the device Example log output: ``` Airship takeOff succeeded Channel ID: 01234567-89ab-cdef-0123-456789abcdef ``` 3. **Verify in the Airship dashboard**: 1. Open your project. 1. Go to **Audience**, then **Contact Management**. 1. Search for the Channel ID from your logs. The channel should appear with the correct platform, iOS or Android. If you don't see a channel ID or encounter errors during initialization, see the [Troubleshooting](https://www.airship.com/docs/developer/sdk-integration/flutter/troubleshooting/) guide for common problems and solutions. ## Next steps Now that you've successfully integrated the Airship SDK: 1. [**Enable push notifications**](https://www.airship.com/docs/developer/sdk-integration/flutter/push-notifications/getting-started/) and configure notification handling 2. [**Identify users**](https://www.airship.com/docs/developer/sdk-integration/flutter/audience/contacts/) with contacts and named users 3. [**Set up Message Center**](https://www.airship.com/docs/developer/sdk-integration/flutter/message-center/getting-started/) for persistent messaging 4. [**Configure analytics**](https://www.airship.com/docs/developer/sdk-integration/flutter/data-collection/analytics/) to track user behavior If you don't see a channel ID or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/flutter/troubleshooting/initialization/) for common problems and solutions. # Logging > Configure log levels and privacy settings to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. By default, the log level is set to **Info** for development builds and **Error** for production builds to ensure clean logs in a live environment. ## Log levels The following log levels are available, ordered from most to least verbose. | Log Level | Description | | :-------- | :---------- | | **Verbose** | Reports highly detailed SDK status, which is useful for deep debugging and troubleshooting. | | **Debug** | Reports general SDK status with more detailed information than `Info`. | | **Info** | Reports general SDK status and lifecycle events. | | **Warning** | Used for API deprecations, invalid setup, and other potentially problematic situations that are generally recoverable. | | **Error** | Used for critical errors, exceptions, and other situations that the SDK cannot gracefully handle. | | **None** | Disables all logging. | ## Configure log levels You can set the log level in the Airship config when calling `takeOff`. This setting acts as a minimum threshold—only logs at that level and higher will be output. ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, // Set log level for both platforms logLevel: LogLevel.verbose, // Optional: Set platform-specific log privacy levels iosConfig: IOSConfig( logPrivacyLevel: IOSLogPrivacyLevel.public ), androidConfig: AndroidConfig( logPrivacyLevel: AndroidLogPrivacyLevel.public ) ); Airship.takeOff(config); runApp(MyApp()); } ``` ### Recommended log levels by environment | Environment | Recommended Level | Purpose | | :---------- | :--------------- | :------ | | **Development** | `LogLevel.verbose` or `LogLevel.debug` | Maximum detail for debugging | | **Staging/QA** | `LogLevel.info` | General status information | | **Production** | `LogLevel.error` or `LogLevel.warning` | Only critical issues | ### Example: Conditional logging Set different log levels based on build mode: ```dart import 'package:flutter/foundation.dart'; var config = AirshipConfig( // ... other config ... // Use verbose logging in debug builds, errors only in release logLevel: kDebugMode ? LogLevel.verbose : LogLevel.error, iosConfig: IOSConfig( logPrivacyLevel: kDebugMode ? IOSLogPrivacyLevel.public : IOSLogPrivacyLevel.private ), androidConfig: AndroidConfig( logPrivacyLevel: kDebugMode ? AndroidLogPrivacyLevel.public : AndroidLogPrivacyLevel.private ) ); ``` ## Log privacy levels For better security in production environments, you can control the visibility of log contents using privacy levels. This is especially useful when you need to debug a release build without exposing sensitive information like channel IDs, named users, or custom event data. ### Private (default) This is the default setting, designed to protect data in a production environment. Sensitive information is redacted from log output. **Use case**: Production builds where log output might be collected or viewed by support teams. ### Public This setting increases log visibility, making it easier to capture detailed information from release builds. Sensitive information is visible in logs. To ensure visibility in production builds, `verbose` and `debug` messages are automatically elevated to the `info` log level. **Use case**: Development builds or when actively debugging issues in test environments. > **Warning:** Only use `public` privacy level in development or controlled test environments. Never use it in production builds that will be distributed to end users, as it may expose sensitive user data in logs. ## Viewing logs View Airship SDK logs in your platform's native development tools. ### iOS logs View iOS logs in Xcode's console: 1. Run your app in Xcode 2. Open the **Debug area** (View → Debug Area → Show Debug Area or ⇧⌘Y) 3. Filter logs by typing "Airship" in the search field ### Android logs View Android logs using Logcat: 1. Run your app in Android Studio 2. Open **Logcat** (View → Tool Windows → Logcat) 3. Filter logs by selecting your app's package or searching for "Airship" Or use the command line: `adb logcat | grep Airship` ## Example log output With verbose logging enabled, you'll see detailed output like: ``` [Airship] Airship takeOff succeeded [Airship] Channel ID: 01234567-89ab-cdef-0123-456789abcdef [Airship] Push notifications enabled: true [Airship] Analytics tracking event: screen_viewed ``` ## Troubleshooting logging issues If you're not seeing expected log output: * **Verify log level**: Ensure you've set an appropriate log level (e.g., `LogLevel.verbose` for debugging) * **Check privacy level**: If using release builds, set privacy level to `public` for full visibility * **Filter console output**: Use "Airship" as a filter keyword in your IDE's console * **Restart the app**: Log configuration is set during `takeOff`, so restart the app after making changes # Locale > Configure locale behavior and override the default locale that Airship uses for messaging and analytics. The Airship SDK is localized in 48 different languages for all strings included within the SDK. By default, the SDK automatically uses the device's or app's configured [Locale](https://www.airship.com/docs/reference/glossary/#locale). Airship uses the locale for various locale-sensitive operations: * **Message localization**: Selecting the appropriate language variant for push notifications, in-app messages, and Message Center content * **Analytics reporting**: Recording the user's locale for segmentation and reporting * **SDK strings**: Displaying SDK-generated UI elements in the user's language Apps can override the locale to use a different locale than the device's current setting. This is useful when your app provides its own language selector or needs to match Airship's locale to your app's locale setting. ## Override the locale You can override the locale programmatically at runtime, which takes precedence over the device's locale settings. Use standard locale identifiers (e.g., "en", "en-US", "de", "fr-CA"). ```dart // Set locale to German Airship.locale.setLocaleOverride("de"); // Set locale to Canadian French Airship.locale.setLocaleOverride("fr-CA"); // Set locale to US English Airship.locale.setLocaleOverride("en-US"); ``` > **Note:** Locale overrides persist across app sessions. Once set, the override remains active until explicitly cleared or changed. ## Clear the locale override To remove a locale override and return to using the device's locale: ```dart Airship.locale.clearLocaleOverride(); ``` ## Get the current locale To retrieve the locale that Airship is currently using: ```dart var locale = await Airship.locale.locale; debugPrint("Current Airship locale: $locale"); ``` ## Example: Sync with app language If your app provides a language selector, you can sync Airship's locale with the user's selection: ```dart // When user changes language in your app void onLanguageChanged(String languageCode) { // Update your app's locale // ... your app-specific code ... // Update Airship's locale to match Airship.locale.setLocaleOverride(languageCode); debugPrint("Language changed to: $languageCode"); } // When user resets to device default void onResetToDeviceLanguage() { // Clear Airship's locale override Airship.locale.clearLocaleOverride(); debugPrint("Reset to device locale"); } ``` # Advanced Configuration > Configure URL allowlists and other advanced settings for the Airship Flutter SDK. This guide covers advanced configuration options for the Airship Flutter SDK. ## URL Allowlist The URL allowlist is a security feature that controls which URLs the Airship SDK can open or interact with. This prevents malicious or unintended URLs from being accessed through your app. The SDK divides URL usage into three different config options: * **urlAllowListScopeOpenUrl**: Controls URLs that can be opened from actions, landing pages, HTML in-app messages, or In-App Automation media. By default, allows Airship-originated URLs and YouTube URLs. * **urlAllowListScopeJavaScriptInterface**: Controls which URLs can have the Airship JavaScript interface injected into webviews. By default, allows only Airship-originated URLs. * **urlAllowList**: Applies both scopes to the specified URLs. ### URL pattern syntax ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` ### Example allowlist configurations ```dart var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, // Allow all URLs (use with caution in production) urlAllowList: ["*"] ); ``` ```dart // Allow specific domains var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, urlAllowList: [ "https://yourwebsite.com", "https://*.yourwebsite.com/*" // All subdomains ] ); ``` ```dart // Allow specific URL patterns with different scopes var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, urlAllowListScopeOpenUrl: [ "https://yourwebsite.com/*", "https://youtube.com/*", "yourapp://*" // Deep link scheme ], urlAllowListScopeJavaScriptInterface: [ "https://yourwebsite.com/*" ] ); ``` > **Warning:** Setting `urlAllowList` to `["*"]` allows the SDK to open any URL. While convenient for development, this should be carefully considered for production environments. Only include specific domains you trust. > **Note:** These config options are passed to `Airship.takeOff()` during SDK initialization. See the [Flutter Setup guide](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/) for complete initialization instructions. # Extend Airship > How to extend the Airship Flutter plugin to access native iOS and Android SDK features not exposed through the Flutter API. You can provide a plugin extender that will be automatically loaded for the app. The extender can be used to modify the Airship config before SDK initialization and to access the underlying native SDK once Airship is ready. This gives the app a chance to customize parts of Airship that are not configurable through the Flutter plugin, such as setting up [iOS Live Activities](https://www.airship.com/docs/developer/sdk-integration/flutter/live-activities/) and [Android Live Updates](https://www.airship.com/docs/developer/sdk-integration/flutter/live-updates/). ## iOS For iOS, create a Swift file named `AirshipPluginExtender.swift` and needs to be included in the main app target. Make sure the class has the `@objc(AirshipPluginExtender)` annotation and inherits `AirshipPluginExtenderProtocol`. ```swift import Foundation import AirshipKit import AirshipFrameworkProxy import ActivityKit @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { public static func onAirshipReady() { // Called when Airship is ready on the MainActor } public static func extendConfig(config: inout AirshipConfig) { // Called to extend the AirshipConfig before SDK initialization } } ``` ## Android Create a file in the App's src directory named `AirshipExtender`. It needs to extend `com.urbanairship.android.framework.proxy.AirshipPluginExtender` and have an empty constructor. ```kotlin // Replace with your package package com.example import android.content.Context import androidx.annotation.Keep import com.urbanairship.AirshipConfigOptions import com.urbanairship.UAirship import com.urbanairship.android.framework.proxy.AirshipPluginExtender @Keep public final class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { // Called when Airship is ready on a background thread. // Avoid doing long running, blocking work or it will delay Airship } override fun extendConfig( context: Context, configBuilder: AirshipConfigOptions.Builder ): AirshipConfigOptions.Builder { // Called to extend the AirshipConfig before SDK initialization return configBuilder } } ``` Register the extender in the manifest: ```xml ``` ### Push Notifications Configure and handle push notifications in your Flutter app for iOS and Android. # Push Notifications > Set up push notifications for Flutter applications on iOS and Android. Before you can send and receive push notifications, you need to configure your app for the platform(s) you're targeting. ## Platform Setup Follow the platform-specific setup instructions below to enable push notifications in your Flutter app. ### iOS Configure iOS capabilities and extensions to enable push notifications. #### Enable Push Notifications Capability 1. Open your Flutter project's iOS module in Xcode: * Navigate to your Flutter project directory * Run `open ios/Runner.xcworkspace` (or open the workspace file manually) 2. Select your app target in Xcode, then go to **Signing & Capabilities**. 3. If you do not see Push Notifications enabled, click **+ Capability** and add **Push Notifications**. ![Adding the Push Notifications capability in Xcode](https://www.airship.com/docs/images/ios-enable-push-notifications-capabilities_hu_2e1789fffb02612b.webp) *Adding the Push Notifications capability in Xcode* #### Enable Background Modes 1. Select your app target and then click the **Signing & Capabilities** tab. 2. Click **+ Capability** and add **Background Modes**. ![Adding the Background Modes capability in Xcode](https://www.airship.com/docs/images/ios-enable-background-mode-capabilities_hu_f135d9fec0ba0d06.webp) *Adding the Background Modes capability in Xcode* 3. In the **Background Modes** section, select the **Remote notifications** checkbox. ![Enabling Remote notifications in Background Modes](https://www.airship.com/docs/images/ios-background-mode-remote-notifications_hu_7e38b08288fcd7b2.webp) *Enabling Remote notifications in Background Modes* #### Notification Service Extension

To take advantage of notification attachments, such as images, animated gifs, and video, you will need to create a notification service extension.

Follow the steps in the [iOS Notification Service Extension Guide](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/). ### Android Configure Firebase Cloud Messaging (FCM) or Huawei Mobile Services (HMS) to enable push notifications on Android. #### FCM Setup 1. If you haven't already, create a Firebase project and add your Android app in the [Firebase Console](https://console.firebase.google.com/). 2. Download the `google-services.json` configuration file from your Firebase project. 3. Place `google-services.json` in your Flutter project at `android/app/google-services.json`. #### Configure Gradle 1. Add the Google Services plugin to your project-level `build.gradle` file (`android/build.gradle`): ```gradle buildscript { dependencies { // Add this line classpath 'com.google.gms:google-services:4.3.15' } } ``` 2. Apply the Google Services plugin in your app-level `build.gradle` file (`android/app/build.gradle`): ```gradle apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'dev.flutter.flutter-gradle-plugin' // Add this line at the bottom apply plugin: 'com.google.gms.google-services' ``` #### Notification Configuration Configure the notification icon and accent color in your `takeOff` config: ```dart var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, androidConfig: AndroidConfig( notificationConfig: AndroidNotificationConfig( icon: "ic_notification", accentColor: "#00ff00" ) ) ); Airship.takeOff(config); ``` See the [Flutter Setup guide](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/) for complete `takeOff` configuration options. ## Enable User Notifications By default, user notifications are disabled to give you control over when to request permission. ### Request notification permission Enable user notifications to prompt for permission: ```dart // Enable user notifications (prompts for permission) await Airship.push.setUserNotificationsEnabled(true); ``` ### When to request permission **Don't prompt immediately**: Requesting notification permission on app launch typically results in low opt-in rates because users don't yet understand your app's value. **Wait for context**: Request permission when users understand the benefit. Good times include: * After completing onboarding * When a user wants to be notified about specific content * After a user takes an action that would benefit from notifications * When explaining the value notifications provide ### Example: Contextual permission request ```dart Future requestNotificationPermission(BuildContext context) async { // Show a dialog explaining the value of notifications bool? shouldEnable = await showDialog( context: context, builder: (context) => AlertDialog( title: Text('Stay Updated'), content: Text( 'Enable notifications to receive important updates ' 'and special offers directly on your device.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text('Not Now'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: Text('Enable'), ), ], ), ); if (shouldEnable == true) { await Airship.push.setUserNotificationsEnabled(true); } } ``` > **Note:** **Android 13+ (API 33)**: On Android 13 and above, enabling user notifications displays a runtime permission prompt. If users deny permission, they must manually enable notifications in system settings. > > To maximize opt-in rates, avoid prompting immediately on app startup and instead wait for an appropriate moment when users understand the value of notifications. ## Check Notification Status Check if notifications are currently enabled on Airship: ```dart bool enabled = await Airship.push.isUserNotificationsEnabled; print('User notifications enabled: $enabled'); ``` For a detailed breakdown of notification status: ```dart PushNotificationStatus? status = await Airship.push.notificationStatus; print('User notifications enabled: ${status?.isUserNotificationsEnabled}'); print('System notifications allowed: ${status?.areNotificationsAllowed}'); print('Push privacy feature enabled: ${status?.isPushPrivacyFeatureEnabled}'); print('Push token registered: ${status?.isPushTokenRegistered}'); print('Fully opted in: ${status?.isOptedIn}'); ``` The notification status provides: * `isUserNotificationsEnabled`: If user notifications are enabled on Airship * `areNotificationsAllowed`: If notifications are allowed at the system level * `isPushPrivacyFeatureEnabled`: If the push feature is enabled on Privacy Manager * `isPushTokenRegistered`: If push registration was able to generate a token * `isOptedIn`: If Airship is able to send and display push notifications (requires all of the above) See the [Troubleshooting](https://www.airship.com/docs/developer/sdk-integration/flutter/troubleshooting/#push-notification-issues) guide for help diagnosing notification issues. ## Get Push Token For debugging or integration purposes, you can access the device's push token: ```dart // Get the push token (FCM registration token on Android, APNs device token on iOS) String? pushToken = await Airship.push.registrationToken; print('Push token: $pushToken'); ``` Listen for token changes: ```dart Airship.push.onPushTokenReceived.listen((event) { print('Push token received: ${event.pushToken}'); }); ``` ## Next Steps * [**Handling Notification Events**](https://www.airship.com/docs/developer/sdk-integration/flutter/push-notifications/handling-notification-events/) — Listen for push received and notification response events * [**Customizing Notifications**](https://www.airship.com/docs/developer/sdk-integration/flutter/push-notifications/customizing-notifications/) — Configure iOS notification options, badges, and foreground presentation If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/flutter/troubleshooting/push-notifications/) to check notification status and fix common issues. # Notification Events > Learn how to listen for push received events, notification responses, and implement background message handling. The Airship SDK provides event streams to listen for push notifications and user interactions. These callbacks allow you to perform custom processing, navigate to specific content, or update your app's state. ## Listen for Push Received Events The `onPushReceived` stream fires when a push notification is received while your app is in the foreground or background: ```dart import 'dart:async'; import 'package:airship_flutter/airship_flutter.dart'; StreamSubscription? _pushSubscription; @override void initState() { super.initState(); // Listen for push notifications _pushSubscription = Airship.push.onPushReceived.listen((event) { print('Push received:'); print('Alert: ${event.pushPayload.alert}'); print('Extras: ${event.pushPayload.extras}'); // Handle the push notification // e.g., show an in-app banner, update UI, etc. }); } @override void dispose() { _pushSubscription?.cancel(); super.dispose(); } ``` > **Note:** **Android**: The `onPushReceived` listener is not called when the app is terminated. For terminated app states on Android, use the [background message handler](#android-background-message-handler) instead. ## Listen for Notification Responses The `onNotificationResponse` stream fires when a user interacts with a notification (taps it, taps an action button, or dismisses it): ```dart StreamSubscription? _responseSubscription; @override void initState() { super.initState(); // Listen for notification interactions _responseSubscription = Airship.push.onNotificationResponse.listen((event) { print('Notification tapped:'); print('Action ID: ${event.actionId}'); // null for default tap print('Alert: ${event.pushPayload.alert}'); // Navigate to specific content based on the push if (event.pushPayload.extras['deep_link'] != null) { String deepLink = event.pushPayload.extras['deep_link']; _navigateToDeepLink(deepLink); } }); } void _navigateToDeepLink(String deepLink) { // Navigate to the appropriate screen Navigator.pushNamed(context, deepLink); } @override void dispose() { _responseSubscription?.cancel(); super.dispose(); } ``` ## Listen for Notification Status Changes Monitor changes to the notification status: ```dart StreamSubscription? _statusSubscription; @override void initState() { super.initState(); _statusSubscription = Airship.push.onNotificationStatusChanged.listen((event) { print('Notification status changed:'); print('Opted in: ${event.status.isOptedIn}'); print('System allowed: ${event.status.areNotificationsAllowed}'); }); } @override void dispose() { _statusSubscription?.cancel(); super.dispose(); } ``` ## Complete Example Here's a complete example showing how to handle both push received and notification response events: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; import 'dart:async'; class NotificationHandler extends StatefulWidget { @override _NotificationHandlerState createState() => _NotificationHandlerState(); } class _NotificationHandlerState extends State { StreamSubscription? _pushSubscription; StreamSubscription? _responseSubscription; String _lastNotification = 'No notifications yet'; @override void initState() { super.initState(); _setupNotificationListeners(); } void _setupNotificationListeners() { // Handle push received (foreground/background) _pushSubscription = Airship.push.onPushReceived.listen((event) { setState(() { _lastNotification = 'Received: ${event.pushPayload.alert ?? "No alert"}'; }); // Show an in-app notification ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(event.pushPayload.alert ?? 'New notification')), ); }); // Handle notification interaction _responseSubscription = Airship.push.onNotificationResponse.listen((event) { setState(() { _lastNotification = 'Tapped: ${event.pushPayload.alert ?? "No alert"}'; }); // Handle deep links or custom actions Map extras = event.pushPayload.extras; if (extras.containsKey('screen')) { Navigator.pushNamed(context, extras['screen']); } else if (extras.containsKey('url')) { // Open URL with url_launcher package // launch(extras['url']); } }); } @override void dispose() { _pushSubscription?.cancel(); _responseSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Notifications')), body: Center( child: Text(_lastNotification), ), ); } } ``` ## Android Background Message Handler For Android, you must set up a background message handler to receive push notifications when the app is completely terminated (not running in the foreground or background). ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; // Background message handler (must be a top-level function) Future backgroundMessageHandler(PushReceivedEvent event) async { print('Received background push:'); print('Alert: ${event.pushPayload.alert}'); print('Extras: ${event.pushPayload.extras}'); // Perform background work // Note: Keep this lightweight and fast // Avoid UI operations or heavy processing } void main() { WidgetsFlutterBinding.ensureInitialized(); // Register the background message handler (Android only) Airship.push.android.setBackgroundPushReceivedHandler(backgroundMessageHandler); // Initialize Airship var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, ); Airship.takeOff(config); runApp(MyApp()); } ``` > **Important:** **Background handler requirements:** > * Must be a top-level function (not a class method) > * Should complete quickly (within a few seconds) > * Avoid UI operations or long-running tasks > * Cannot access `BuildContext` or app state directly > * Only applies to Android (iOS handles background notifications differently) ## Working with Push Payloads The `PushPayload` object contains all notification data: ```dart Airship.push.onNotificationResponse.listen((event) { PushPayload payload = event.pushPayload; // Standard fields print('Alert: ${payload.alert}'); print('Title: ${payload.title}'); print('Subtitle: ${payload.subtitle}'); print('Notification ID: ${payload.notificationId}'); // Custom data Map extras = payload.extras; if (extras.containsKey('product_id')) { String productId = extras['product_id']; // Navigate to product details } }); ``` # Customize Notifications > Configure iOS notification options, badges, foreground presentation, and silent notifications. Customize how notifications appear and behave on iOS and Android platforms. ## iOS Notification Options By default, the Airship SDK will request `Alert`, `Badge`, and `Sound` notification options for remote notifications. You can customize these options by setting them before enabling user notifications: ```dart Airship.push.iOS.setNotificationOptions([ IOSNotificationOption.alert, IOSNotificationOption.badge, IOSNotificationOption.sound, ]); // Then enable user notifications await Airship.push.setUserNotificationsEnabled(true); ``` ### Available Options * `IOSNotificationOption.alert` - Display alerts * `IOSNotificationOption.badge` - Update the app badge * `IOSNotificationOption.sound` - Play sounds * `IOSNotificationOption.carPlay` - Display notifications in CarPlay * `IOSNotificationOption.criticalAlert` - Display critical alerts (requires special entitlement) * `IOSNotificationOption.providesAppNotificationSettings` - Indicates the app has custom notification settings * `IOSNotificationOption.provisional` - Enables provisional authorization ## Provisional Authorization Apps can request provisional authorization along with the usual notification options. When requesting provisional authorization, apps do not need to prompt the user for permission initially, and notifications will be delivered in a non-interruptive manner to the Notification Center until the user explicitly chooses to keep delivering messages either prominently or quietly. ```dart Airship.push.iOS.setNotificationOptions([ IOSNotificationOption.alert, IOSNotificationOption.badge, IOSNotificationOption.sound, IOSNotificationOption.provisional, ]); // Enable notifications (no prompt will be shown) await Airship.push.setUserNotificationsEnabled(true); ``` > **Note:** Provisional notifications appear in Notification Center but not as banners or with sounds. Users can then choose to keep or turn off notifications from Notification Center. ## iOS Foreground Presentation Options When a push is received in the foreground on iOS, how the notification is displayed to the user is controlled by foreground presentation options. By default, the SDK will not set any options so the notification will be silenced. ```dart Airship.push.iOS.setForegroundPresentationOptions([ IOSForegroundPresentationOption.banner, IOSForegroundPresentationOption.list, IOSForegroundPresentationOption.sound, ]); ``` ### Available Presentation Options * `IOSForegroundPresentationOption.sound` - Play the sound associated with the notification * `IOSForegroundPresentationOption.badge` - Apply the notification's badge value to the app's icon * `IOSForegroundPresentationOption.list` - Show the notification in Notification Center (and as a banner on iOS 13 and older) * `IOSForegroundPresentationOption.banner` - Present the notification as a banner (and in Notification Center on iOS 13 and older) > **Note:** If no foreground presentation options are set, notifications received while the app is in the foreground will be silently received without displaying any UI to the user. ## iOS Badge Management The badge on iOS presents a counter on top of the application icon. You can control this directly through Airship: ```dart // Set badge number await Airship.push.iOS.setBadge(20); // Get current badge number int currentBadge = await Airship.push.iOS.badge; print('Current badge: $currentBadge'); // Reset badge await Airship.push.iOS.resetBadge(); ``` ### Auto-Badge Auto-badge automatically increments the badge when a notification is received and decrements it when the notification is cleared: ```dart // Enable auto-badge await Airship.push.iOS.setAutoBadgeEnabled(true); // Check if auto-badge is enabled bool isEnabled = await Airship.push.iOS.isAutoBadgeEnabled(); ``` > **Important:** When using auto-badge, only modify the badge value through Airship methods to ensure the value stays in sync. ## iOS Authorized Notification Settings Check which notification settings the user has authorized: ```dart // Get authorized notification settings List settings = await Airship.push.iOS.authorizedNotificationSettings; print('Authorized settings: $settings'); // Get authorization status IOSAuthorizedNotificationStatus status = await Airship.push.iOS.authorizedNotificationStatus; print('Authorization status: $status'); ``` Listen for changes to authorized settings: ```dart Airship.push.iOS.onAuthorizedSettingsChanged.listen((event) { print('Authorized settings changed: ${event.authorizedSettings}'); }); ``` ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. ### Platform-Specific Behavior * **Android**: All push messages are delivered in the background. By default, Airship treats messages without an `alert` as silent. * **iOS**: Set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject) when sending the push. ### Example Silent Notification Payload ```json { "audience": "all", "notification": { "ios": { "content_available": true, "extra": { "update_type": "content_refresh" } }, "android": { "extra": { "update_type": "content_refresh" } } }, "device_types": ["ios", "android"] } ``` > **Note:** Pushes sent with the `content_available` property (iOS) or without an `alert` (Android) do not have guaranteed delivery. Factors affecting delivery include battery life, whether the device is connected to WiFi, and the number of silent pushes sent within a recent time period. These metrics are determined solely by iOS/Android and APNs/FCM. Therefore, this feature is best used for supplementing the regular behavior of the app rather than providing critical functionality. For instance, an app could use a silent push to pre-fetch new data ahead of time in order to reduce load times when the app is later launched by the user. ## Clear Notifications Clear notifications programmatically: ```dart // Clear all notifications await Airship.push.clearNotifications(); // Clear a specific notification by ID await Airship.push.clearNotification('notification_id'); // Get active notifications List activeNotifications = await Airship.push.activeNotifications; print('Active notifications: ${activeNotifications.length}'); ``` > **Note:** On Android, this only clears notifications sent through Airship. On iOS, it clears all notifications for the app. ### In-App Experiences Configure and control In-App Experiences in Flutter applications. # In-App Experiences > Pause, resume, and control display timing for In-App Experiences. In-App Experiences are automatically enabled when you integrate the Airship SDK. Use these methods to control when and how they are displayed. ## Pausing and Resuming Display You can pause and resume In-App Experiences to control when they are displayed to users. ```dart // Pause in-app experiences await Airship.inApp.setPaused(true); // Resume in-app experiences await Airship.inApp.setPaused(false); // Check if paused bool isPaused = await Airship.inApp.isPaused(); ``` ### Auto-Pause on Launch You can configure the SDK to automatically pause In-App Experiences on launch. This is useful if you want to defer showing In-App Experiences until after onboarding or other critical app flows. ```dart var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, autoPauseInAppAutomationOnLaunch: true ); Airship.takeOff(config); ``` See the [Flutter Setup guide](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/) for complete `takeOff` configuration options. When you're ready to display In-App Experiences, call `setPaused(false)`: ```dart await Airship.inApp.setPaused(false); ``` ## Display Interval Control the minimum time between In-App Experience displays to avoid overwhelming users. ```dart // Set display interval to 5 seconds (5000 milliseconds) await Airship.inApp.setDisplayInterval(5000); // Get current display interval int interval = await Airship.inApp.getDisplayInterval(); ``` The display interval is the minimum time (in milliseconds) that must pass between displaying In-App Experiences. # Embedded Content > Integrate Embedded Content into your Flutter app to display Scene content directly within your app's screens. For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see [Embedded Content]({{< ref "/guides/features/messaging/scenes/embedded-content.md" >}}). ## Adding an embedded view The `AirshipEmbeddedView` widget defines a place for Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `embeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```dart import 'package:airship_flutter/airship_flutter.dart'; // Show any "home_banner" Embedded Content AirshipEmbeddedView(embeddedId: "home_banner") ``` ## Sizing The `AirshipEmbeddedView` accepts optional `parentWidth` and `parentHeight` parameters for controlling its dimensions. If not provided, the widget uses the available width and height. Use `parentHeight` for a constant height instead of a height-constrained container, as this allows the view to properly collapse to zero height when content is dismissed. **Custom sizing** ```dart AirshipEmbeddedView( embeddedId: "home_banner", parentWidth: 400, parentHeight: 200, ) ``` ## Checking if embedded content is available Use `Airship.inApp.isEmbeddedAvailable` to check if embedded content is currently available for a given ID: ```dart import 'package:airship_flutter/airship_flutter.dart'; final isAvailable = Airship.inApp.isEmbeddedAvailable(embeddedId: "home_banner"); ``` ## Listening for embedded content updates Use `Airship.inApp.isEmbeddedAvailableStream` to observe changes in the availability of embedded content for a given ID. The stream emits a boolean indicating whether content is available to display. It immediately emits the current state upon subscription, then emits updates whenever the state changes. **Availability stream** ```dart import 'package:airship_flutter/airship_flutter.dart'; final subscription = Airship.inApp .isEmbeddedAvailableStream(embeddedId: "home_banner") .listen((isAvailable) { print("home_banner is available: $isAvailable"); }); // Cancel the subscription when no longer needed subscription.cancel(); ``` ## Listing all pending embedded content Use `Airship.inApp.getEmbeddedInfos()` to get a list of all `EmbeddedInfo` objects across all embedded IDs that are pending display. Pending content has been triggered and prepared but has not yet been displayed in an `AirshipEmbeddedView`. Each `EmbeddedInfo` contains the `embeddedId` it is associated with. ```dart List allPending = Airship.inApp.getEmbeddedInfos(); ``` To observe changes to pending embedded content, use the `onEmbeddedInfoUpdated` stream: ```dart Airship.inApp.onEmbeddedInfoUpdated.listen((List infos) { print("Pending embedded infos updated: $infos"); }); ``` ## Showing a placeholder when content is unavailable The `AirshipEmbeddedView` collapses to zero height when no content is available. Use `isEmbeddedAvailableStream` to toggle between a placeholder and the embedded view based on content availability. **Embedded view with placeholder** ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class HomeBanner extends StatelessWidget { const HomeBanner({super.key}); @override Widget build(BuildContext context) { return StreamBuilder( // The stream emits the current state immediately upon subscription stream: Airship.inApp.isEmbeddedAvailableStream(embeddedId: "home_banner"), builder: (context, snapshot) { final isAvailable = snapshot.data ?? false; if (!isAvailable) { // Display a placeholder when no content is available return const SizedBox( height: 200, child: Center(child: Text("No content available")), ); } // Display the Airship content when it becomes available return const AirshipEmbeddedView( embeddedId: "home_banner", parentHeight: 200, ); }, ); } } ``` # Custom Views > Register custom native views to use within Scenes. A *Custom View* is a native view from your mobile or web application embedded into a Scene. Custom Views can display any native content your app exposes, so you can reuse that existing content within any screen in a Scene. Custom Views allow you to embed native iOS and Android views within Scenes, giving you full control over design and layout while leveraging Airship's targeting and orchestration capabilities. ## Requirements To use Custom Views in Flutter, you must add native iOS and Android code to your Flutter project. Custom Views work by embedding Flutter widgets within native Airship custom view containers. ## Implementation Custom Views require native implementation on both iOS and Android platforms. For a complete working implementation, see this [Flutter Custom Views example](https://gist.github.com/rlepinski/e67f917114222e4529811e43f319a7ce). The basic pattern is: 1. **Set up Flutter routing** to handle custom view routes (e.g., `/custom/my-view`) 2. **Register custom views on iOS** using `AirshipCustomViewManager` 3. **Register custom views on Android** using `AirshipCustomViewManager` ### Flutter Routing Configure your app's routing to handle custom view paths: ```dart MaterialApp( onGenerateRoute: (settings) { // Pattern match on full route paths switch (settings.name) { case '/custom/my-banner': return MaterialPageRoute( builder: (context) => Material(child: MyBannerWidget()), ); case '/custom/my-product-card': return MaterialPageRoute( builder: (context) => Material(child: MyProductCard()), ); } // Handle other routes return null; }, home: MyHomePage(), ) ``` ### iOS Create `AirshipPluginExtender.swift` in your `ios/Runner/` directory: ```swift import Foundation import AirshipFrameworkProxy import ActivityKit import Flutter #if canImport(AirshipCore) import AirshipCore import AirshipAutomation #else import AirshipKit #endif @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { @MainActor public static func onAirshipReady() { AirshipCustomViewManager.shared.register(name: "example") { args in FlutterCustomViewWrapper(viewName: "example", properties: args.properties) } } } ``` Create `FlutterCustomView.swift` in your `ios/Runner/` directory: ```swift import Flutter import UIKit import SwiftUI import AirshipFrameworkProxy #if canImport(AirshipCore) import AirshipCore import AirshipAutomation #else import AirshipKit #endif /// SwiftUI wrapper for Flutter custom view @available(iOS 16.0, *) public struct FlutterCustomView: View { let viewName: String let properties: AirshipJSON? public var body: some View { FlutterCustomViewRepresentable(viewName: viewName, properties: properties) } } /// UIViewRepresentable bridge for SwiftUI @available(iOS 16.0, *) struct FlutterCustomViewRepresentable: UIViewRepresentable { let viewName: String let properties: AirshipJSON? func makeUIView(context: Context) -> FlutterCustomViewContainer { return FlutterCustomViewContainer(viewName: viewName, properties: properties) } func updateUIView(_ uiView: FlutterCustomViewContainer, context: Context) { // No updates needed } } /// Flutter custom view that embeds a Flutter widget public class FlutterCustomViewContainer: UIView { private let viewName: String private let properties: AirshipJSON? private var flutterEngine: FlutterEngine? private var flutterViewController: FlutterViewController? public init(viewName: String, properties: AirshipJSON?) { self.viewName = viewName self.properties = properties super.init(frame: .zero) setupView() } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupView() { backgroundColor = .systemGray6 clipsToBounds = true } override public func willMove(toWindow newWindow: UIWindow?) { super.willMove(toWindow: newWindow) if newWindow != nil { embedFlutterView() } else { removeFlutterView() } } override public func layoutSubviews() { super.layoutSubviews() flutterViewController?.view.frame = bounds } private func embedFlutterView() { flutterEngine = FlutterEngine(name: "airship_custom_\(viewName)") let result = flutterEngine?.run() guard result == true else { return } flutterViewController = FlutterViewController( engine: flutterEngine!, nibName: nil, bundle: nil ) guard let flutterViewController = flutterViewController else { return } addSubview(flutterViewController.view) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let self = self else { return } let route = "/custom/\(self.viewName)" self.flutterViewController?.pushRoute(route) } } private func removeFlutterView() { flutterViewController?.view.removeFromSuperview() flutterViewController = nil flutterEngine?.destroyContext() flutterEngine = nil } } ``` For more details on iOS custom views, see the [Apple Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/custom-views/). ### Android Create `AirshipExtender.kt` in your `android/app/src/main/kotlin/` directory: ```kotlin import android.content.Context import android.view.View import android.widget.FrameLayout import io.flutter.embedding.android.FlutterView import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.dart.DartExecutor import com.urbanairship.android.layout.AirshipCustomViewManager import com.urbanairship.android.layout.AirshipCustomViewHandler import com.urbanairship.android.layout.AirshipCustomViewArguments import android.util.Log import io.flutter.embedding.android.FlutterTextureView import androidx.annotation.Keep import com.urbanairship.UAirship import com.urbanairship.android.framework.proxy.AirshipPluginExtender @Keep class AirshipExtender : AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { AirshipCustomViewManager.register("example", FlutterCustomViewHandler()) } } class FlutterCustomViewHandler : AirshipCustomViewHandler { override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View { return FlutterCustomView( context, args.name, args.properties ) } } class FlutterCustomView( context: Context, private val viewName: String, private val properties: com.urbanairship.json.JsonMap ) : FrameLayout(context) { private var flutterEngine: FlutterEngine? = null private var flutterView: FlutterView? = null private var isEngineInitialized = false companion object { private const val TAG = "FlutterCustomView" } init { setupView() } private fun setupView() { setBackgroundColor(android.graphics.Color.BLACK) if (layoutParams == null) { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) } } override fun onAttachedToWindow() { super.onAttachedToWindow() embedFlutterView() } private fun embedFlutterView() { if (isEngineInitialized) { return } try { val route = "/custom/$viewName" flutterEngine = FlutterEngine(context).apply { navigationChannel.setInitialRoute(route) dartExecutor.executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ) } val renderSurface = FlutterTextureView(context) flutterView = FlutterView(context, renderSurface) val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) addView(flutterView, params) flutterView?.attachToFlutterEngine(flutterEngine!!) flutterEngine?.lifecycleChannel?.appIsResumed() isEngineInitialized = true } catch (e: Exception) { Log.e(TAG, "Failed to create Flutter view", e) cleanup() } } override fun onDetachedFromWindow() { super.onDetachedFromWindow() cleanup() } override fun onWindowVisibilityChanged(visibility: Int) { super.onWindowVisibilityChanged(visibility) when (visibility) { View.VISIBLE -> { flutterEngine?.lifecycleChannel?.appIsResumed() } View.INVISIBLE, View.GONE -> { flutterEngine?.lifecycleChannel?.appIsPaused() } } } private fun cleanup() { try { flutterEngine?.lifecycleChannel?.appIsPaused() flutterView?.let { view -> view.detachFromFlutterEngine() removeView(view) } flutterView = null flutterEngine?.destroy() flutterEngine = null isEngineInitialized = false } catch (e: Exception) { Log.e(TAG, "Error during cleanup", e) } } } ``` For more details on Android custom views, see the [Android Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/custom-views/). ## Using Custom Views Once registered, Custom Views can be added to Scenes in the Airship dashboard: 1. Create or edit a Scene 2. Add the **Custom View** content element to a screen 3. Enter the view name (e.g., `my-custom-view`) that matches the name you registered in your native code 4. Optionally add key-value pairs to pass custom properties to the view The native view will be displayed within the Scene with the properties you configured. ### Message Center Implement Message Center in your Flutter app to provide an inbox for persistent, rich messages. # Message Center > Learn how to display Message Center, manage messages, and implement common inbox functionality in your Flutter app. Message Center provides an inbox for rich, HTML-based messages. Learn more about Message Center in our [feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Display the Message Center The Flutter plugin provides a simple way to display the built-in Message Center UI. ### Using the default UI Display the built-in Message Center UI with a single method call. This is perfect for quickly adding Message Center functionality without custom design work: ```dart // Display the default Message Center UI Airship.messageCenter.display(); ``` This method can be called from anywhere in your app: ```dart // Example: Add a button to your app bar AppBar( title: Text('My App'), actions: [ IconButton( icon: Icon(Icons.inbox), onPressed: () { Airship.messageCenter.display(); }, ), ], ) ``` ### Display a specific message You can also display a specific message directly by providing its message ID: ```dart Airship.messageCenter.display(messageId: "specific-message-id"); ``` To customize the Message Center UI or navigation, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/flutter/message-center/embedding/). ## Fetch messages Retrieve all messages from the inbox: ```dart List messages = await Airship.messageCenter.messages; // Display messages in your UI for (var message in messages) { print('Message: ${message.title}'); print('ID: ${message.id}'); print('Unread: ${message.unread}'); print('Sent date: ${message.sentDate}'); } ``` ### InboxMessage properties Each `InboxMessage` contains: * **id**: Unique identifier for the message * **title**: Message title * **sentDate**: When the message was sent * **expirationDate**: When the message expires (optional) * **unread**: Whether the message is unread * **extras**: Custom key-value pairs associated with the message ## Listen for message updates Subscribe to real-time message updates using streams. This is useful for updating your UI when new messages arrive or existing messages are modified: ```dart StreamSubscription? inboxSubscription; @override void initState() { super.initState(); // Listen for inbox updates inboxSubscription = Airship.messageCenter.onInboxUpdated.listen((event) { setState(() { // Reload messages when the inbox is updated _loadMessages(); }); }); } Future _loadMessages() async { List messages = await Airship.messageCenter.messages; setState(() { _messages = messages; }); } @override void dispose() { inboxSubscription?.cancel(); super.dispose(); } ``` ## Track unread count Monitor the unread message count to display badges or update UI elements: ### Get current unread count ```dart int unreadCount = await Airship.messageCenter.unreadCount; print('Unread messages: $unreadCount'); ``` ### Listen for unread count changes ```dart // This is useful for updating badges in real-time Airship.messageCenter.onInboxUpdated.listen((event) async { int unreadCount = await Airship.messageCenter.unreadCount; // Update your badge UI _updateBadge(unreadCount); }); ``` ### Example: Show unread badge ```dart class InboxButton extends StatefulWidget { @override _InboxButtonState createState() => _InboxButtonState(); } class _InboxButtonState extends State { int _unreadCount = 0; StreamSubscription? _subscription; @override void initState() { super.initState(); _loadUnreadCount(); // Update badge when inbox changes _subscription = Airship.messageCenter.onInboxUpdated.listen((_) { _loadUnreadCount(); }); } Future _loadUnreadCount() async { int count = await Airship.messageCenter.unreadCount; setState(() { _unreadCount = count; }); } @override void dispose() { _subscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return IconButton( icon: Badge( label: _unreadCount > 0 ? Text('$_unreadCount') : null, child: Icon(Icons.inbox), ), onPressed: () { Airship.messageCenter.display(); }, ); } } ``` ## Refresh messages Manually refresh the message list from the server. This is useful for implementing pull-to-refresh functionality: ```dart // Refresh messages bool success = await Airship.messageCenter.refreshInbox(); if (success) { print('Messages refreshed successfully'); } else { print('Failed to refresh messages'); } ``` ### Example: Pull-to-refresh ```dart RefreshIndicator( onRefresh: () async { await Airship.messageCenter.refreshInbox(); }, child: ListView.builder( itemCount: messages.length, itemBuilder: (context, index) { return MessageListItem(message: messages[index]); }, ), ) ``` ## Mark messages as read Mark one or more messages as read to update their status: ```dart // Mark a single message as read await Airship.messageCenter.markRead(message.id); // Or using the message object directly await Airship.messageCenter.markRead(message.id); ``` Messages are typically marked as read automatically when viewed in the default Message Center UI. For custom implementations, you should mark messages as read when the user views them: ```dart // When user taps on a message void onMessageTapped(InboxMessage message) { // Mark as read Airship.messageCenter.markRead(message.id); // Navigate to message detail screen Navigator.push( context, MaterialPageRoute( builder: (context) => MessageDetailScreen(message: message), ), ); } ``` ## Delete messages Delete messages from the inbox: ```dart // Delete a single message await Airship.messageCenter.deleteMessage(message.id); ``` ### Example: Swipe to delete ```dart Dismissible( key: Key(message.id), direction: DismissDirection.endToStart, onDismissed: (direction) { Airship.messageCenter.deleteMessage(message.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Message deleted')), ); }, background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete, color: Colors.white), ), child: MessageListItem(message: message), ) ``` ## Filter messages by named user By default, Message Center displays all messages sent to the device's channel. If multiple users log into your app on the same device, they'll all see the same messages. To filter messages by named user, you need to: 1. Include a custom key when creating messages with `named_user_id` as the key and the user's actual ID as the value 2. Implement filtering logic in your custom Message Center implementation When creating Message Center messages: * **For the API**: Use the `extra` object in the [Message Center object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#messageobject) * **In the dashboard**: See [Add custom keys](https://www.airship.com/docs/guides/messaging/messages/content/app/message-center/#add-custom-keys) in the Message Center content guide Then filter messages in your Flutter code: ```dart Future> getMessagesForCurrentUser() async { // Get the current named user ID String? currentUserId = await Airship.contact.namedUserId; if (currentUserId == null) { return []; // No user logged in } // Get all messages List allMessages = await Airship.messageCenter.messages; // Filter messages that match the current user return allMessages.where((message) { // Check if the message has a named_user_id extra String? messageUserId = message.extras['named_user_id']; return messageUserId == null || messageUserId == currentUserId; }).toList(); } ``` # Embed the Message Center > Build custom Message Center UIs with full control over design, navigation, and functionality using the InboxMessageView widget and Message Center APIs. This guide covers creating fully custom Message Center implementations for Flutter applications, giving you complete control over the design, navigation, and user experience. ## Override Default Display Behavior To use a custom Message Center implementation instead of the default UI, disable auto-launch and listen for display events: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); // Disable the default Message Center UI Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false); // Listen for display events and navigate to custom UI Airship.messageCenter.onDisplay.listen((event) { // Navigate to your custom Message Center screen Navigator.push( context, MaterialPageRoute( builder: (context) => CustomMessageCenterScreen( messageId: event.messageId, // null for full inbox, or specific message ID ), ), ); }); } @override Widget build(BuildContext context) { return MaterialApp( home: HomeScreen(), ); } } ``` > **Note:** When you disable the default Message Center, you're responsible for displaying your own UI when the `onDisplay` event fires. This includes handling both full inbox views and individual message views. ## Why customize Message Center While the default Message Center UI works great for many apps, you might want to customize it for: * **Brand consistency**: Match your app's unique design language and visual style * **Custom navigation**: Integrate Message Center into your app's existing navigation patterns * **Enhanced functionality**: Add search, filtering, categorization, or other custom features * **Multi-user support**: Filter messages by named user when multiple users share a device * **Platform-specific design**: Create different experiences for iOS and Android ## Custom Message Center implementation To build a custom Message Center, disable the default UI and use the Message Center APIs to manage messages programmatically. ### Step 1: Disable the default UI First, disable the default Message Center so you can display your own: ```dart // In your app initialization Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false); ``` ### Step 2: Listen for display events Handle display events to show your custom UI when Message Center is triggered: ```dart Airship.messageCenter.onDisplay.listen((event) { // event.messageId will be null for full inbox, or contain a specific message ID Navigator.push( context, MaterialPageRoute( builder: (context) => CustomMessageCenterScreen( messageId: event.messageId, ), ), ); }); ``` ### Step 3: Build your custom UI Create your custom Message Center screen using the Message Center APIs: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; import 'dart:async'; class CustomMessageCenterScreen extends StatefulWidget { final String? messageId; const CustomMessageCenterScreen({Key? key, this.messageId}) : super(key: key); @override _CustomMessageCenterScreenState createState() => _CustomMessageCenterScreenState(); } class _CustomMessageCenterScreenState extends State { List _messages = []; int _unreadCount = 0; bool _isLoading = true; StreamSubscription? _inboxSubscription; @override void initState() { super.initState(); _loadMessages(); // Listen for inbox updates _inboxSubscription = Airship.messageCenter.onInboxUpdated.listen((_) { _loadMessages(); }); } Future _loadMessages() async { setState(() { _isLoading = true; }); try { List messages = await Airship.messageCenter.messages; int unreadCount = await Airship.messageCenter.unreadCount; setState(() { _messages = messages; _unreadCount = unreadCount; _isLoading = false; }); // If a specific message was requested, open it if (widget.messageId != null) { InboxMessage? message = _messages.firstWhere( (m) => m.id == widget.messageId, orElse: () => null as InboxMessage, ); if (message != null) { _openMessage(message); } } } catch (e) { setState(() { _isLoading = false; }); print('Error loading messages: $e'); } } Future _refreshMessages() async { await Airship.messageCenter.refreshInbox(); } void _openMessage(InboxMessage message) { // Mark as read Airship.messageCenter.markRead(message.id); // Navigate to message detail Navigator.push( context, MaterialPageRoute( builder: (context) => MessageDetailScreen(message: message), ), ); } Future _deleteMessage(InboxMessage message) async { await Airship.messageCenter.deleteMessage(message.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Message deleted')), ); } @override void dispose() { _inboxSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Messages'), actions: [ if (_unreadCount > 0) Center( child: Padding( padding: EdgeInsets.only(right: 16), child: Chip( label: Text('$_unreadCount unread'), backgroundColor: Theme.of(context).primaryColor, labelStyle: TextStyle(color: Colors.white), ), ), ), ], ), body: _isLoading ? Center(child: CircularProgressIndicator()) : _messages.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inbox, size: 64, color: Colors.grey), SizedBox(height: 16), Text( 'No messages', style: Theme.of(context).textTheme.headlineSmall, ), ], ), ) : RefreshIndicator( onRefresh: _refreshMessages, child: ListView.builder( itemCount: _messages.length, itemBuilder: (context, index) { InboxMessage message = _messages[index]; return Dismissible( key: Key(message.id), direction: DismissDirection.endToStart, onDismissed: (direction) { _deleteMessage(message); }, background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete, color: Colors.white), ), child: ListTile( leading: CircleAvatar( backgroundColor: message.unread ? Theme.of(context).primaryColor : Colors.grey, child: Icon( message.unread ? Icons.mail : Icons.drafts, color: Colors.white, ), ), title: Text( message.title ?? 'No title', style: TextStyle( fontWeight: message.unread ? FontWeight.bold : FontWeight.normal, ), ), subtitle: Text( _formatDate(message.sentDate), style: TextStyle(fontSize: 12), ), trailing: Icon(Icons.chevron_right), onTap: () => _openMessage(message), ), ); }, ), ), ); } String _formatDate(DateTime date) { DateTime now = DateTime.now(); Duration difference = now.difference(date); if (difference.inDays == 0) { return 'Today ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; } else if (difference.inDays == 1) { return 'Yesterday'; } else if (difference.inDays < 7) { return '${difference.inDays} days ago'; } else { return '${date.month}/${date.day}/${date.year}'; } } } ``` ## Using the InboxMessageView widget The `InboxMessageView` widget displays individual Message Center messages with their HTML content. Use this widget to create custom message detail screens: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class MessageDetailScreen extends StatefulWidget { final InboxMessage message; const MessageDetailScreen({Key? key, required this.message}) : super(key: key); @override _MessageDetailScreenState createState() => _MessageDetailScreenState(); } class _MessageDetailScreenState extends State { InboxMessageViewController? _controller; void _onInboxMessageViewCreated(InboxMessageViewController controller) { _controller = controller; controller.loadMessage(widget.message); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.message.title ?? 'Message'), actions: [ IconButton( icon: Icon(Icons.delete), onPressed: () { // Delete and go back Airship.messageCenter.deleteMessage(widget.message.id); Navigator.pop(context); }, ), ], ), body: InboxMessageView( onViewCreated: _onInboxMessageViewCreated, ), ); } } ``` ### InboxMessageView properties The `InboxMessageView` widget provides: * **Automatic HTML rendering**: Displays rich HTML content from Message Center messages * **Link handling**: Automatically handles links within message content * **Action execution**: Processes Airship actions embedded in messages * **Responsive layout**: Adapts to different screen sizes ## Advanced: Message filtering Filter messages based on custom criteria, such as named user or categories: ```dart class FilteredMessageCenterScreen extends StatefulWidget { @override _FilteredMessageCenterScreenState createState() => _FilteredMessageCenterScreenState(); } class _FilteredMessageCenterScreenState extends State { List _filteredMessages = []; String? _currentCategory; Future _loadAndFilterMessages() async { // Get all messages List allMessages = await Airship.messageCenter.messages; // Get current named user String? namedUserId = await Airship.contact.namedUserId; // Filter messages List filtered = allMessages.where((message) { // Filter by named user if set if (namedUserId != null) { String? messageUserId = message.extras['named_user_id']; if (messageUserId != null && messageUserId != namedUserId) { return false; } } // Filter by category if set if (_currentCategory != null) { String? messageCategory = message.extras['category']; if (messageCategory != _currentCategory) { return false; } } return true; }).toList(); setState(() { _filteredMessages = filtered; }); } void _setCategory(String? category) { setState(() { _currentCategory = category; }); _loadAndFilterMessages(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Messages'), actions: [ PopupMenuButton( onSelected: _setCategory, itemBuilder: (context) => [ PopupMenuItem(value: null, child: Text('All')), PopupMenuItem(value: 'promotions', child: Text('Promotions')), PopupMenuItem(value: 'updates', child: Text('Updates')), PopupMenuItem(value: 'alerts', child: Text('Alerts')), ], ), ], ), body: ListView.builder( itemCount: _filteredMessages.length, itemBuilder: (context, index) { return MessageListItem(message: _filteredMessages[index]); }, ), ); } } ``` ## Best practices When implementing custom Message Center: 1. **Always mark messages as read**: When a user views a message, mark it as read to keep the unread count accurate 2. **Handle empty states**: Show helpful messages when the inbox is empty 3. **Implement pull-to-refresh**: Let users manually refresh their messages 4. **Show loading indicators**: Provide feedback while fetching messages 5. **Clean up subscriptions**: Cancel stream subscriptions in `dispose()` to prevent memory leaks 6. **Handle errors gracefully**: Show user-friendly error messages if message loading fails 7. **Test with multiple users**: If implementing named user filtering, thoroughly test the filtering logic ### Preference Center Let users manage their notification preferences and subscription lists with Preference Center. # Preference Center > Preference Center allows users to opt in and out of subscription lists configured via the Airship Dashboard. > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. Preference Center provides a pre-built UI for users to manage their subscription preferences. Learn more in the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Display a Preference Center Display a Preference Center with a single method call: ```dart Airship.preferenceCenter.display("preference-center-id"); ``` To build a custom Preference Center UI, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/flutter/preference-center/embedding/). # Embed the Preference Center > Create custom Preference Center UIs by fetching the config and building your own subscription management interface. This guide covers creating custom Preference Center UIs for Flutter applications. Unlike the default Preference Center, you'll build your own UI from scratch using the Preference Center configuration and subscription list APIs. ## Override Default Display Behavior To use a custom Preference Center instead of the default UI, disable auto-launch for the specific Preference Center ID and handle display events: ```dart // Disable the OOTB UI for this Preference Center Airship.preferenceCenter.setAutoLaunchDefaultPreferenceCenter( "preference-center-id", false ); // Add a listener to handle display events StreamSubscription? subscription; subscription = Airship.preferenceCenter.onDisplay.listen((event) { final preferenceCenterId = event.preferenceCenterId; // Navigate to your custom preference center UI navigateToCustomPreferenceCenter(preferenceCenterId); }); ``` ## Fetching Preference Center Config The Preference Center config contains all the information needed to build your UI, including subscription lists, sections, and display settings. ```dart PreferenceCenterConfig config = await Airship.preferenceCenter.getConfig("preference-center-id"); ``` > **Note:** The config might not be available immediately on first app start. Implement exponential backoff if automatically retrying, or provide a UI for users to manually retry. ## Building Your Custom UI You'll need to: 1. **Fetch the config** to get the list of subscription lists and their current state 2. **Build your UI** using the config data (sections, subscription lists, display settings) 3. **Update subscription lists** when users make changes using the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/flutter/audience/subscription-lists/) ### Example Implementation ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class CustomPreferenceCenterScreen extends StatefulWidget { final String preferenceCenterId; const CustomPreferenceCenterScreen({ Key? key, required this.preferenceCenterId, }) : super(key: key); @override _CustomPreferenceCenterScreenState createState() => _CustomPreferenceCenterScreenState(); } class _CustomPreferenceCenterScreenState extends State { PreferenceCenterConfig? _config; bool _loading = true; Map _subscriptions = {}; @override void initState() { super.initState(); _loadConfig(); } Future _loadConfig() async { try { final config = await Airship.preferenceCenter.getConfig( widget.preferenceCenterId, ); // Load current subscription status final currentSubs = await Airship.contact.subscriptionLists; final subsMap = {}; for (var section in config.sections) { for (var item in section.items) { if (item.subscriptionId != null) { subsMap[item.subscriptionId!] = currentSubs.contains(item.subscriptionId); } } } setState(() { _config = config; _subscriptions = subsMap; _loading = false; }); } catch (error) { print('Failed to load config: $error'); setState(() { _loading = false; }); } } Future _toggleSubscription(String listId, bool subscribe) async { try { if (subscribe) { await Airship.contact.editSubscriptionLists() .subscribe(listId) .apply(); } else { await Airship.contact.editSubscriptionLists() .unsubscribe(listId) .apply(); } setState(() { _subscriptions[listId] = subscribe; }); } catch (error) { print('Failed to update subscription: $error'); } } @override Widget build(BuildContext context) { if (_loading) { return Scaffold( appBar: AppBar(title: Text('Loading...')), body: Center(child: CircularProgressIndicator()), ); } if (_config == null) { return Scaffold( appBar: AppBar(title: Text('Error')), body: Center( child: Text('Failed to load Preference Center'), ), ); } return Scaffold( appBar: AppBar( title: Text(_config!.display?.name ?? 'Preferences'), ), body: ListView.builder( itemCount: _config!.sections.length, itemBuilder: (context, sectionIndex) { final section = _config!.sections[sectionIndex]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (section.display?.name != null) Padding( padding: EdgeInsets.all(16), child: Text( section.display!.name!, style: Theme.of(context).textTheme.titleLarge, ), ), ...section.items.map((item) { final subscriptionId = item.subscriptionId; if (subscriptionId == null) return SizedBox.shrink(); return SwitchListTile( title: Text(item.display?.name ?? ''), subtitle: item.display?.description != null ? Text(item.display!.description!) : null, value: _subscriptions[subscriptionId] ?? false, onChanged: (value) { _toggleSubscription(subscriptionId, value); }, ); }).toList(), Divider(), ], ); }, ), ); } } ``` > **Important:** Preference Center configuration is currently limited to subscription lists only. Use the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/flutter/audience/subscription-lists/) to manage user subscriptions. ### Audience Management Integrate audience management features into your Flutter app to identify users, set attributes, manage tags, and control subscription lists for targeted messaging. # Channels > Access and manage channel IDs and listen for channel creation. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. ```dart // Get the channel ID (may return null if not yet created) String? channelId = await Airship.channel.identifier; // Wait for the channel ID to be created (returns the channel ID once available) String channelId = await Airship.channel.waitForChannelId(); ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Use `waitForChannelId()` if you need to wait for the channel to be created before proceeding. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. ```dart Airship.channel.onChannelCreated.listen((event) { debugPrint('Channel created $event'); }); ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring `AirshipConfig`, see [Flutter SDK Setup](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/). ```dart Airship.takeOff( AirshipConfig( ... isChannelCaptureEnabled: false ) ); ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during takeOff. For more information about Privacy Manager, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. ```dart Airship.contact.identify(namedUserId); ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. ```dart Airship.contact.reset(); ``` You can get the Named User ID only if you set it through the SDK. ```dart await Airship.contact.namedUserId; ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. ```dart Airship.channel.addTags(["flutter"]); Airship.channel.removeTags(["some-tag"]); // Accessing channel tags List tags = await Airship.channel.tags; ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```dart Airship.channel.editTagGroups() ..addTags("loyalty", ["silver-member"]) ..removeTags("loyalty", ["bronze-member"]) ..apply() ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```dart Airship.contact.editTagGroups() ..addTags("loyalty", ["silver-member"]) ..removeTags("loyalty", ["bronze-member"]) ..apply() ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. ```dart Airship.channel.editAttributes() ..setAttribute("device_name", "Bobby's Phone") ..setAttribute("average_rating", 4.99) ..removeAttribute("vip_status") ..apply() ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. ```dart Airship.contact.editAttributes() ..setAttribute("first_name", "Bobby") ..apply() ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. ```dart Airship.contact.editAttributes() ..setJsonAttribute("attribute_name", "instance_id", {"key":"value", "another_key":"another_value"}) ..removeJsonAttribute("some_attribute_name", "some_instance_id") ..apply() ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. ```dart // Modifying channel subscription lists Airship.channel.editSubscriptionLists() ..subscribe("food") ..unsubscribe("sports") ..apply(); // Fetching channel subscription lists List channelSubscriptions = await Airship.channel.subscriptionLists; ``` ## Contact Subscription Lists Contact subscriptions are set at the user-level and require a Channel scope specifying the types that the subscription list applies to. ```dart // Modifying contact subscription lists Airship.contact.editSubscriptionLists() ..subscribe("food", ChannelScope.app) ..unsubscribe("sports", ChannelScope.sms) ..apply(); // Fetching contact subscription lists Map> contactSubscriptions = await Airship.contact.subscriptionLists; ``` ## Verifying Subscription Lists To verify that subscription lists have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ### Data Collection Overview of data collection and controls provided by the Airship Flutter SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: | Privacy Manager Flag | Features | |----------------------|----------| | `push` | Push notifications | | `in_app_automation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | | `message_center` | Message Center | | `tags_and_attributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | | `contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | | `analytics` | Associated identifiers, Custom events, Screen tracking, Surveys, email address, Feature Flag interaction | | `feature_flags` | Feature Flag evaluation and interaction | | `all` | All features | | `none` | No features | ## Configuring default enabled features You can configure which features are enabled by default when the SDK initializes. This is done in your Airship config during `takeOff`. For information about setting up the Airship SDK and configuring `AirshipConfig`, see [Flutter SDK Setup](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/). ### Default configuration (all features enabled) By default, all features are enabled. The SDK will collect data and make network requests as configured. ### Disabling all features To disable all features by default (useful for consent opt-in flows), set the enabled features to an empty array: ```dart Airship.takeOff( AirshipConfig( defaultEnvironment: ConfigEnvironment( enabledFeatures: [] ) ) ); ``` ### Enabling specific features To enable only specific features by default: ```dart Airship.takeOff( AirshipConfig( defaultEnvironment: ConfigEnvironment( enabledFeatures: ["push", "analytics"] ) ) ); ``` ## Modifying enabled features at runtime You can enable or disable features at any time after takeOff. Once you modify the enabled features from the default, those settings are persisted between app launches. ### Enabling features ```dart Airship.privacyManager.enableFeatures(["push", "analytics"]); ``` ### Disabling features ```dart Airship.privacyManager.disableFeatures(["push", "analytics"]); ``` ## Consent opt-in flow example A common use case is to start with all features disabled, then enable them as users grant consent: ```dart // Start with all features disabled Airship.takeOff( AirshipConfig( defaultEnvironment: ConfigEnvironment( enabledFeatures: [] ) ) ); // Later, when user grants consent: void userGrantedConsent() { // Enable features based on user's consent choices Airship.privacyManager.enableFeatures(["push", "analytics"]); } ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Apple Privacy Manifest](https://www.airship.com/docs/reference/data-collection/apple-privacy-manifest/) - Declare data collection practices to Apple (iOS) - [Google Play Data Safety](https://www.airship.com/docs/reference/data-collection/google-play-data-safety/) - Reference for Google Play's Data Safety section (Android) - [Analytics](https://www.airship.com/docs/developer/sdk-integration/flutter/data-collection/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/flutter/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with custom events. They require enabling analytics for your app. For detailed information, see the [Custom Events guide](https://www.airship.com/docs/guides/audience/events/custom-events/). ```dart CustomEvent event = CustomEvent( name: "event_name", value: 123.12, properties: { "my_custom_property": "some custom value", "is_neat": true, "any_json": { "foo": "bar" } } ); Airship.analytics.recordCustomEvent(event); ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. ```dart Airship.analytics.associateIdentifier("key", "value"); ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. ```dart Airship.analytics.trackScreen("MainScreen"); ``` ### Troubleshooting Common issues and solutions for Airship plugin setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/) or [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/advanced-configuration/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Installation Errors If you encounter errors during installation: - Verify that you're using a compatible version of Flutter. See [Requirements](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/#requirements) in *Getting Started*. - Ensure all native dependencies are properly linked. - Run `flutter clean` and `flutter pub get` to refresh dependencies. - For iOS, run `pod install` in the `ios` directory. - Check that your iOS and Android projects are correctly configured. ## Initialization Errors If you encounter errors during SDK initialization: - Verify your credentials in the Airship dashboard. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. - Ensure `WidgetsFlutterBinding.ensureInitialized()` runs first in `main()`, then `Airship.takeOff`, then `runApp()`. - Ensure `site` in your `AirshipConfig` is `Site.us` for US cloud projects or `Site.eu` for EU cloud projects. # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/flutter/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. ## Push Notifications Not Working If push notifications are not being received: - Verify that push notifications are enabled for both iOS and Android. - Check that APNs (iOS) and FCM (Android) are properly configured. - Ensure the app has notification permissions. - For iOS, verify capabilities are enabled in Xcode (Push Notifications and Background Modes). - For Android, ensure `google-services.json` is in `android/app/`. ## Checking Push Notification Status If push notifications aren't working as expected, you can check the notification status to diagnose the issue: ```dart PushNotificationStatus? status = await Airship.push.notificationStatus; print('User notifications enabled: ${status?.isUserNotificationsEnabled}'); print('System notifications allowed: ${status?.areNotificationsAllowed}'); print('Push privacy feature enabled: ${status?.isPushPrivacyFeatureEnabled}'); print('Push token registered: ${status?.isPushTokenRegistered}'); print('User opted in: ${status?.isUserOptedIn}'); print('Fully opted in: ${status?.isOptedIn}'); ``` You can also listen for status changes: ```dart Airship.push.onNotificationStatusChanged.listen((event) { print('Notification status changed: ${event.status}'); print('Is opted in: ${event.status.isOptedIn}'); }); ``` ## Common Status Scenarios - `isUserOptedIn = false`: Check if `userNotificationsEnabled` is set to `true` and if the user granted permission - `isPushPrivacyFeatureEnabled = false`: Push privacy feature is disabled in Privacy Manager - `isPushTokenRegistered = false`: Device hasn't received a push token yet. Check network connectivity and platform configuration - `isUserOptedIn = true` but `isOptedIn = false`: Push token registration is pending or failed. Check console logs for errors ## React Native Integrate the Airship SDK into your React Native applications for iOS and Android. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Message Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/react-native/message-center/getting-started/) and [Preference Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/react-native/preference-center/getting-started/). ```ts Airship.addListener(EventType.DeepLink, (event) => { const deepLink = event.deepLink; // Handle deep link }); ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Running Actions You can run actions programmatically using the `Airship.actions.run()` method. The method returns a Promise that resolves with the action result. The action value can be a string, number, boolean, null, object, or array. **Running an action** ```typescript // Run an action with a string value using async/await try { const result = await Airship.actions.run("action_name", "action_value"); console.log("Action result:", result); } catch (error) { console.error("Action error:", error); } // Run an action with an object value try { const result = await Airship.actions.run("action_name", { key: "value", number: 42 }); console.log("Action result:", result); } catch (error) { console.error("Action error:", error); } // Run an action without a value try { const result = await Airship.actions.run("action_name"); console.log("Action result:", result); } catch (error) { console.error("Action error:", error); } // Run an action using Promise.then() Airship.actions.run("action_name", "action_value") .then((result) => { console.log("Action result:", result); }) .catch((error) => { console.error("Action error:", error); }); ``` ## Custom Actions You can register custom actions with an [Airship extender](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/extending-airship/) and with native SDK APIs. See the native platform documentation for details: - [iOS Actions](https://www.airship.com/docs/developer/sdk-integration/apple/actions/) - [Android Actions](https://www.airship.com/docs/developer/sdk-integration/android/actions/) # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. ```ts const flag = await Airship.featureFlagManager.flag("YOUR_FLAG_NAME"); if (flag.isEligible) { // Do something with the flag } else { // Disable feature or use default behavior } ``` ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/react-native/data-collection/privacy-manager/). ```ts await Airship.featureFlagManager.trackInteraction(flag); ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. ```ts try { await Airship.featureFlagManager.flag("YOUR_FLAG_NAME"); } catch(error) { // Do something with the error } ``` # Live Activities > Integrate Live Activities into your React Native app to display real-time updates on the iOS Lock Screen and Dynamic Island. {{< badge "axp" >}} For the push API method, see the [iOS Live Activities](https://www.airship.com/docs/guides/messaging/features/ios-live-activities/) messaging guide. See also the [iOS Live Activities](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ### App setup Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/extending-airship/), make a call to `LiveActivityManager.shared.setup` to configure any Live Activities for the app. Call `configurator.register` for each Live Activity type that your application defines and include a block on how to parse the name of the activity that you will use to track on Airship. This name will be used to send updates through APNS. ```swift import Foundation import AirshipKit import AirshipFrameworkProxy import ActivityKit // This class header is required to be automatically picked up by the Airship plugin: @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { public static func onAirshipReady() { if #available(iOS 16.1, *) { // Will throw if called more than once try? LiveActivityManager.shared.setup { configurator in // Call for each Live Activity type await configurator.register(forType: Activity.self) { attributes in // Track this property as the Airship name for updates attributes.gameID } } } // other setup } } ``` > **Important:** If you are using Expo, you must copy-paste your exact `ActivityAttributes` struct into your `AirshipPluginExtender.swift` so the compiler can find a definition in order to register the Live Activity. ### Starting Live Activities For any Live Activities configured, you can start a new one using the `start` method: ```typescript Airship.iOS.liveActivityManager.start({ attributesType: 'SportsActivityAttributes', content: { state: { status: 'Game Pending', }, relevanceScore: 0.0, }, attributes: { gameID: 'sports-game-123', }, }); ``` ### Updating Live Activities To update, use `update`, but you will need the activity ID. ```typescript const activities = await Airship.iOS.liveActivityManager.listAll(); const activity = activities.find( (activity) => activity.attributes.gameID === 'sports-game-123' ); if (activity) { Airship.iOS.liveActivityManager.update({ activityId: activity.id, content: { state: { status: "Game starting!" } relevanceScore: 0.0, }, }); } ``` ### Ending Live Activities To end is similar to `update`. Use `end` with the activity ID: ```typescript const activities = await Airship.iOS.liveActivityManager.listAll(); const activity = activities.find( (activity) => activity.attributes.gameID === 'sports-game-123' ); if (activity) { Airship.iOS.liveActivityManager.end({ activityId: activity.id, dismissalPolicy: { type: "default" } }); } ``` # Live Updates > Integrate Live Updates into your React Native app to update content in real-time without requiring an app update. {{< badge "axp" >}} For the push API method, see the [Android Live Updates](https://www.airship.com/docs/guides/messaging/features/android-live-updates/) messaging guide. See also the [Android Live Updates](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ### Creating a handler The Airship SDK supports two types of Live Update handlers: * `NotificationLiveUpdateHandler` — Displays a notification with a custom layout, with content updated by the Live Update. * `CustomLiveUpdateHandler` — Receives Live Update events and provides flexibility to display content using a custom implementation. This can be used to power home screen widgets, views embedded in the app, and more. Each handler type has two different interfaces that may be implemented, to support suspending or callback-based code: * `SuspendLiveUpdateNotificationHandler` * `CallbackLiveUpdateNotificationHandler` * `SuspendLiveUpdateCustomHandler` * `CallbackLiveUpdateCustomHandler` The following `SampleLiveUpdateHandler` reads content from the Live Update payload and displays scores for a sports game in a custom notification layout, using `RemoteViews`: ```kotlin class SampleLiveUpdateHandler : SuspendLiveUpdateNotificationHandler() { override suspend fun onUpdate( context: Context, event: LiveUpdateEvent, update: LiveUpdate ): LiveUpdateResult { // Read content_state fields from the Live Update payload val teamOneScore = update.content.opt("team_one_score").getInt(0).toString() val teamTwoScore = update.content.opt("team_two_score").getInt(0).toString() val statusUpdate = update.content.opt("status_update").optString() // Expanded notification layout val bigLayout = RemoteViews(context.packageName, R.layout.sports_big).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) setTextViewText(R.id.statusUpdate, statusUpdate) } // Collapsed notification layout val smallLayout = RemoteViews(context.packageName, R.layout.sports_small).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) } // Create the notification builder val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_EVENT) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(smallLayout) .setCustomBigContentView(bigLayout) // Return 'ok' with the notification builder. // The Airship SDK will handle posting the notification. // Returning LiveUpdateResult.cancel() will end the Live Update and dismiss the notification. return LiveUpdateResult.ok(builder) } companion object { private const val NOTIFICATION_CHANNEL_ID = "sports" } } ``` ### Registering a handler Handlers must be registered with `LiveUpdateManager` in order to receive Live Update events. This should be done *once* after `takeOff`. Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/extending-airship/), register the types. ```kotlin @Keep public final class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { LiveUpdateManager.shared().run { register(type = "notification", handler = SampleLiveUpdateHandler()) } } } ``` > **Note:** The `type` used above, `"notification"`, is used to map Live Update events to the corresponding handler in your app. > The value can be any string that is unique across all handlers registered by an app. This also allows a single handler to manage multiple Live Updates that each have a unique `name`. ### Starting Live Updates Live Updates can be started from within the app. ```typescript Airship.android.liveUpdateManager.start({ name: 'sports-game-123' type: 'notification', content: { team_one_score: 0, team_two_score: 0, status_update: 'Game started!' } }); ``` ### Updating Live Updates Live Updates can be updated from within the app. ```typescript Airship.android.liveUpdateManager.update({ name: 'sports-game-123' content: { team_one_score: 3, team_two_score: 0, status_update: 'Game started!' } }); ``` ### Ending Live Updates You can end a Live Update from within the app. ```typescript Airship.android.liveUpdateManager.end({ name: 'sports-game-123' content: { team_one_score: 9, team_two_score: 6, status_update: 'Game over!' } }); ``` ### Clearing all active Live Updates During development, it can be useful to reset Live Update tracking on app launch. This allows any Live Updates to be started fresh, even if they were already started during a previous launch. To end all currently active Live Updates, call the `clearAll()` method on `LiveUpdateManager`. ```typescript Airship.android.liveUpdateManager.clearAll(); ``` # React Native Module Changelog > The latest updates to the Airship React Native module. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 26.5.0 May 2, 2026 Minor release that updates the Android SDK to 20.7.0 and the iOS SDK to 20.7.0. ### Changes - Updated Android SDK to [20.7.0](https://github.com/urbanairship/android-library/releases/tag/20.7.0) - Updated iOS SDK to [20.7.0](https://github.com/urbanairship/ios-library/releases/tag/20.7.0) ## 25.4.3 April 23, 2026 Patch release that updates the iOS SDK to 19.11.8 to fix Xcode 26.4 build issues with whole module optimization. ### Changes - Updated iOS SDK to [19.11.8](https://github.com/urbanairship/ios-library/releases/tag/19.11.8) ## 26.4.1 April 11, 2026 Patch release that fixes a `UIViewControllerHierarchyInconsistency` crash in the Airship embedded view wrapper on iOS when an embedded view is pushed and popped on a navigation stack. ### Changes - Fixed iOS embedded view wrapper to properly remove its hosted `UIHostingController` as a child view controller when its window is detached, avoiding a `UIViewControllerHierarchyInconsistency` crash on re-attachment. ## 25.4.2 April 11, 2026 Patch release that fixes a `UIViewControllerHierarchyInconsistency` crash in the Airship embedded view wrapper on iOS when an embedded view is pushed and popped on a navigation stack. ### Changes - Fixed iOS embedded view wrapper to properly remove its hosted `UIHostingController` as a child view controller when its window is detached, avoiding a `UIViewControllerHierarchyInconsistency` crash on re-attachment. ## 25.4.1 April 2, 2026 Patch release that fixes an Xcode 26.4 Swift compiler crash affecting iOS builds. ### Changes - Updated iOS SDK to [19.11.6](https://github.com/urbanairship/ios-library/releases/tag/19.11.6) to fix a Swift compiler crash ("Failed to produce diagnostic for expression") introduced in Xcode 26.4. ## 24.9.1 April 2, 2026 Patch release that fixes an Xcode 26.4 Swift compiler crash affecting iOS builds. ### Changes - Updated iOS SDK to [19.11.6](https://github.com/urbanairship/ios-library/releases/tag/19.11.6) to fix a Swift compiler crash ("Failed to produce diagnostic for expression") introduced in Xcode 26.4. ## 26.4.0 April 1, 2026 Minor release that updates the Android SDK to 20.6.1 and the iOS SDK to 20.6.0. ### Changes - Updated Android SDK to [20.6.1](https://github.com/urbanairship/android-library/releases/tag/20.6.1) - Updated iOS SDK to [20.6.0](https://github.com/urbanairship/ios-library/releases/tag/20.6.0) ## 26.3.0 March 18, 2026 Minor release that updates the Android SDK to 20.5.0 and the iOS SDK to 20.5.0. ### Changes - Updated Android SDK to [20.5.0](https://github.com/urbanairship/android-library/releases/tag/20.5.0) - Updated iOS SDK to [20.5.0](https://github.com/urbanairship/ios-library/releases/tag/20.5.0) ## 26.2.0 February 3, 2026 Minor release that updates the native SDKs, improves logging, and resolves UI event name conflicts. ### Changes - Updated Android SDK to [20.2.0](https://github.com/urbanairship/android-library/releases/tag/20.2.0) - Updated iOS SDK to [20.3.0](https://github.com/urbanairship/ios-library/releases/tag/20.3.0) - Pinned Swift version to 6.0 for the Airship React Native module - Resolved MessageView UI event name conflicts - Improved logging ## 25.4.0 February 3, 2026 Minor release that updates MessageView event names to avoid collisions and pins the Swift version for iOS builds. No breaking API changes. ### Changes - Namespaced MessageView UI event registration names - Set the Swift version for the Airship React Native module ## 24.9.0 February 3, 2026 Minor release that updates MessageView event names to avoid collisions and pins the Swift version for iOS builds. No breaking API changes. ### Changes - Namespaced MessageView UI event registration names - Set the Swift version for the Airship React Native module ## 26.1.0 January 23, 2026 Minor release that includes accessibility improvements for Message Center and fixes a potential crash on Android. ### Changes - Updated Android SDK to [20.1.1](https://github.com/urbanairship/android-library/releases/tag/20.1.1) - Updated iOS SDK to [20.1.1](https://github.com/urbanairship/ios-library/releases/tag/20.1.1) - Fixed a potential crash in Android Scenes with specific image and display settings. - Improved VoiceOver focus handling for Message Center on iOS. - Fixed an issue where the Message Center title was not being marked as a heading on Android. [Migration Guide](https://github.com/urbanairship/react-native-airship/blob/main/MIGRATION.md) ## 26.0.0 December 11, 2025 This major release updates the native Airship SDKs to 20.0 and adds support for React Native 0.82+. It includes breaking changes—primarily affecting build configurations and native customizations—as well as an updated support policy. For detailed upgrade instructions, please consult the [Migration Guide](https://github.com/urbanairship/react-native-airship/blob/main/MIGRATION.md). ### Changes - Updated Android SDK to [20.0.4](https://github.com/urbanairship/android-library/releases/tag/20.0.4) - Updated iOS SDK to [20.0.2](https://github.com/urbanairship/ios-library/releases/tag/20.0.2) - Added support for React Native 0.82+ - Updated minimum iOS deployment target to 16.0 - Xcode 26+ is now required - Removed deprecated `AirshipExtender` on Android - Removed deprecated forward listener/delegate interfaces - Removed support for React Native old architecture - Removed pre-generated code from the package ## 25.3.1 November 15, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Updated Android SDK to [19.13.6](https://github.com/urbanairship/android-library/releases/tag/19.13.6) - Updated iOS SDK to [19.11.2](https://github.com/urbanairship/ios-library/releases/tag/19.11.2) - Fixed the HeadlessJSService from preventing the app from terminating right away on Android. ## 24.8.1 November 15, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Updated Android SDK to [19.13.6](https://github.com/urbanairship/android-library/releases/tag/19.13.6) - Updated iOS SDK to [19.11.2](https://github.com/urbanairship/ios-library/releases/tag/19.11.2) - Fixed the HeadlessJSService from preventing the app from terminating right away on Android. ## 24.8.0 October 8, 2025 Minor release that updates the Android SDK to 19.13.4 and the iOS SDK to 19.11.0 ### Changes - Updated Android SDK to [19.13.4](https://github.com/urbanairship/android-library/releases/tag/19.13.4) - Updated iOS SDK to [19.11.0](https://github.com/urbanairship/ios-library/releases/tag/19.11.0) ## 25.3.0 October 8, 2025 Minor release that updates the Android SDK to 19.13.4 and the iOS SDK to 19.11.0. ### Changes - Updated Android SDK to [19.13.4](https://github.com/urbanairship/android-library/releases/tag/19.13.4) - Updated iOS SDK to [19.11.0](https://github.com/urbanairship/ios-library/releases/tag/19.11.0) ## 23.6.0 September 17, 2025 Minor release that updates the Android SDK to 19.13.1 and the iOS SDK to 19.9.2. ### Changes - Updated Android SDK to [19.13.1](https://github.com/urbanairship/android-library/releases/tag/19.13.1) - Updated iOS SDK to [19.9.2](https://github.com/urbanairship/ios-library/releases/tag/19.9.2) ## 21.9.0 September 17, 2025 Minor release that updates the Android SDK to 19.13.1 and the iOS SDK to 19.9.2. ### Changes - Updated Android SDK to [19.13.1](https://github.com/urbanairship/android-library/releases/tag/19.13.1) - Updated iOS SDK to [19.9.2](https://github.com/urbanairship/ios-library/releases/tag/19.9.2) ## 25.2.0 September 17, 2025 Minor release that updates the Android SDK to 19.13.1 and the iOS SDK to 19.9.2. ### Changes - Updated Android SDK to [19.13.1](https://github.com/urbanairship/android-library/releases/tag/19.13.1) - Updated iOS SDK to [19.9.2](https://github.com/urbanairship/ios-library/releases/tag/19.9.2) ## 24.7.0 September 17, 2025 Minor release that updates the Android SDK to 19.13.1 and the iOS SDK to 19.9.2. ### Changes - Updated Android SDK to [19.13.1](https://github.com/urbanairship/android-library/releases/tag/19.13.1) - Updated iOS SDK to [19.9.2](https://github.com/urbanairship/ios-library/releases/tag/19.9.2) [Migration Guides](https://github.com/urbanairship/react-native-airship/blob/main/MIGRATION.md) [All Releases](https://github.com/urbanairship/react-native-airship/releases) ## 25.1.0 August 27, 2025 Minor release that updates the Android SDK to 19.11.0 and the iOS SDK to 19.8.3 ### Changes - Updated Android SDK to [19.11.0](https://github.com/urbanairship/android-library/releases/tag/19.11.0) - Updated iOS SDK to [19.8.3](https://github.com/urbanairship/ios-library/releases/tag/19.8.3) - Fixed possible crash when dismissing a Message Center view. ## 24.6.0 August 27, 2025 Minor release that updates the Android SDK to 19.11.0 and the iOS SDK to 19.8.3 ### Changes - Updated Android SDK to [19.11.0](https://github.com/urbanairship/android-library/releases/tag/19.11.0) - Updated iOS SDK to [19.8.3](https://github.com/urbanairship/ios-library/releases/tag/19.8.3) - Fixed possible crash when dismissing a Message Center view. ## 25.0.0 August 21, 2025 Major release to support React Native 0.81. ### Changes - Added support for React Native 0.81 ## 24.5.1 August 20, 2025 Patch release with several bug fixes for Scenes, including an important reporting fix for embedded content. ### Changes - Updated Android SDK to [19.10.2](https://github.com/urbanairship/android-library/releases/tag/19.10.2) - Updated iOS SDK to [19.8.2](https://github.com/urbanairship/ios-library/releases/tag/19.8.2) ## 23.5.1 August 20, 2025 Patch release with several bug fixes for Scenes, including an important reporting fix for embedded content. ### Changes - Updated Android SDK to [19.10.2](https://github.com/urbanairship/android-library/releases/tag/19.10.2) - Updated iOS SDK to [19.8.2](https://github.com/urbanairship/ios-library/releases/tag/19.8.2) ## 21.8.1 August 20, 2025 Patch release with several bug fixes for Scenes, including an important reporting fix for embedded content. ### Changes - Updated Android SDK to [19.10.2](https://github.com/urbanairship/android-library/releases/tag/19.10.2) - Updated iOS SDK to [19.8.2](https://github.com/urbanairship/ios-library/releases/tag/19.8.2) ## 24.5.0 July 31, 2025 Minor release that updates the Android SDK to 19.10.0 and the iOS SDK to 19.7.0 ### Changes - Updated Android SDK to [19.10.0](https://github.com/urbanairship/android-library/releases/tag/19.10.0) - Updated iOS SDK to [19.7.0](https://github.com/urbanairship/ios-library/releases/tag/19.7.0) [Migration Guides](https://github.com/urbanairship/react-native-airship/blob/main/MIGRATION.md) [All Releases](https://github.com/urbanairship/react-native-airship/releases) ## 24.4.0 June 30, 2025 Minor release that updates the Android SDK to 19.9.1 and the iOS SDK to 19.6.1 ### Changes - Updated Android SDK to [19.9.1](https://github.com/urbanairship/android-library/releases/tag/19.9.1 - Updated iOS SDK to [19.6.1](https://github.com/urbanairship/ios-library/releases/tag/19.6.1 - Added Android `logPrivacyLevel` configuration support - Fixed issue with push received pushes when disabling headless JS task before the module initializes ## 23.5.0 June 30, 2025 Minor release that adds support for Android log privacy level configuration and updates the Android SDK to 19.9.1 and the iOS SDK to 19.6.1. The **23.x branch** is now considered **End of Cycle**. This branch supports React Native 0.78.x, which is no longer actively supported by the React Native team. ### Changes - Updated Android SDK to [19.9.1](https://github.com/urbanairship/android-library/releases/tag/19.9.1) - Updated iOS SDK to [19.6.1](https://github.com/urbanairship/ios-library/releases/tag/19.6.1) - Added Android `logPrivacyLevel` configuration support - Fixed issue with push received pushes when disabling headless JS task before the module initializes ## 21.8.0 June 30, 2025 Minor release that adds support for Android log privacy level configuration and updates the Android SDK to 19.9.1 and the iOS SDK to 19.6.1. The **21.x branch** is now considered **Unsupported**. This branch supports React Native 0.77.x and older, which is no longer supported by the React Native team. ### Changes - Updated Android SDK to [19.9.1](https://github.com/urbanairship/android-library/releases/tag/19.9.1) - Updated iOS SDK to [19.6.1](https://github.com/urbanairship/ios-library/releases/tag/19.6.1) - Added Android `logPrivacyLevel` configuration support - Fixed issue with push received pushes when disabling headless JS task before the module initializes ## 24.3.0 May 23, 2025 Minor release focused on performance improvements for Scenes. ### Changes - Updated Android SDK to [19.8.0](https://github.com/urbanairship/android-library/releases/tag/19.8.0) - Updated iOS SDK to [19.5.0](https://github.com/urbanairship/ios-library/releases/tag/19.5.0) ## 23.4.0 May 23, 2025 Minor release focused on performance improvements for Scenes. ### Changes - Updated Android SDK to [19.8.0](https://github.com/urbanairship/android-library/releases/tag/19.8.0) - Updated iOS SDK to [19.5.0](https://github.com/urbanairship/ios-library/releases/tag/19.5.0) ## 21.7.0 May 23, 2025 Minor release focused on performance improvements for Scenes. ### Changes - Updated Android SDK to [19.8.0](https://github.com/urbanairship/android-library/releases/tag/19.8.0) - Updated iOS SDK to [19.5.0](https://github.com/urbanairship/ios-library/releases/tag/19.5.0) ## 24.2.0 May 16, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. - Updated Android SDK to [19.7.0](https://github.com/urbanairship/android-library/releases/tag/19.7.0) - Updated iOS SDK to [19.4.0](https://github.com/urbanairship/ios-library/releases/tag/19.4.0) ## 23.3.0 May 16, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. - Updated Android SDK to [19.7.0](https://github.com/urbanairship/android-library/releases/tag/19.7.0) - Updated iOS SDK to [19.4.0](https://github.com/urbanairship/ios-library/releases/tag/19.4.0) ## 21.6.0 May 16, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. - Updated Android SDK to [19.7.0](https://github.com/urbanairship/android-library/releases/tag/19.7.0) - Updated iOS SDK to [19.4.0](https://github.com/urbanairship/ios-library/releases/tag/19.4.0) ## 24.1.1 May 9, 2025 Patch release with several bug fixes for iOS. ### Changes - Fixed `Airship.preferenceCenter.getConfig(preferenceCenterId)` on iOS - Updated iOS SDK to [19.3.2](https://github.com/urbanairship/ios-library/releases/tag/19.3.2) ## 23.2.1 May 9, 2025 Patch release with several bug fixes for iOS. ### Changes - Fixed `Airship.preferenceCenter.getConfig(preferenceCenterId)` on iOS - Updated iOS SDK to [19.3.2](https://github.com/urbanairship/ios-library/releases/tag/19.3.2) ## 21.5.1 May 9, 2025 Patch release with several bug fixes for iOS. ### Changes - Fixed `Airship.preferenceCenter.getConfig(preferenceCenterId)` on iOS - Updated iOS SDK to [19.3.2](https://github.com/urbanairship/ios-library/releases/tag/19.3.2) ## 24.1.0 May 1, 2025 Minor release that updates the Android SDK to 19.6.2 and the iOS SDK to 19.3.1. ### Changes - Updated Android SDK to [19.6.2](https://github.com/urbanairship/android-library/releases/tag/19.6.2) - Updated iOS SDK to [19.3.1](https://github.com/urbanairship/ios-library/releases/tag/19.3.1) - Added support for JSON attributes - Added new method `Airship.channel.waitForChannelId()` that waits for the channel ID to be created ## 23.2.0 May 1, 2025 Minor release that updates the Android SDK to 19.6.2 and the iOS SDK to 19.3.1. ### Changes - Updated Android SDK to [19.6.2](https://github.com/urbanairship/android-library/releases/tag/19.6.2) - Updated iOS SDK to [19.3.1](https://github.com/urbanairship/ios-library/releases/tag/19.3.1) - Added support for JSON attributes - Added new method `Airship.channel.waitForChannelId()` that waits for the channel ID to be created ## 21.5.0 May 1, 2025 Minor release that updates the Android SDK to 19.6.2 and the iOS SDK to 19.3.1. ### Changes - Updated Android SDK to [19.6.2](https://github.com/urbanairship/android-library/releases/tag/19.6.2) - Updated iOS SDK to [19.3.1](https://github.com/urbanairship/ios-library/releases/tag/19.3.1) - Added support for JSON attributes - Added new method `Airship.channel.waitForChannelId()` that waits for the channel ID to be created ## 24.0.0 April 18, 2025 Major release to support React Native 0.79. ### Changes - Added support for React Native 0.79 ## 23.1.1 April 18, 2025 Patch release that updates the Android SDK to 19.5.1 and the iOS SDK to 19.2.1 ### Changes - Updated Android SDK to [19.5.1](https://github.com/urbanairship/android-library/releases/tag/19.5.1 - Updated iOS SDK to [19.2.1](https://github.com/urbanairship/ios-library/releases/tag/19.2.1 ## 23.1.0 April 8, 2025 Minor release that updates the iOS SDK to 19.2.0 and remove commonjs in favor of ECM only to avoid [dual package hazard](https://nodejs.org/docs/latest-v19.x/api/packages.html#dual-package-hazard). ### Changes - Updated iOS SDK to [19.2.0](https://github.com/urbanairship/ios-library/releases/tag/19.2.0) - Drops commonjs package - Adds back `types` to package.json for apps that are having troubles discovering type definitions - Fixed null notification in Airship.push.iOS.setForegroundPresentationOptionsCallback ## 21.4.1 April 7, 2025 Patch release to fix the notification being null in `Airship.push.iOS.setForegroundPresentationOptionsCallback`. ### Changes - Fixed null notification in `Airship.push.iOS.setForegroundPresentationOptionsCallback` ## 21.4.0 April 4, 2025 Minor release that updates the iOS SDK to 19.2.0 and backports the new `AirshipPluginExtensions`. ### Changes - Updated iOS SDK to [19.2.0](https://github.com/urbanairship/ios-library/releases/tag/19.2.0) - Backported `AirshipPluginExtensions` from 23.0.0 - Deprecated `AirshipPluginForwardListeners` and `AirshipPluginForwardDelegates` ## 21.3.0 April 1, 2025 Minor release that updates the Android SDK to 19.5.0 and the iOS SDK to 19.1.2. ### Changes - Updated Android SDK to [19.5.0](https://github.com/urbanairship/android-library/releases/tag/19.5.0) - Updated iOS SDK to [19.1.2](https://github.com/urbanairship/ios-library/releases/tag/19.1.2) ## 23.0.0 April 1, 2025 Major release that updates the Android SDK to 19.5.0 and the iOS SDK to 19.1.2. The only breaking change is related to the native plugin hooks, which make it easier to integrate the plugin with hybrid apps. Most applications won't be affected. ### Changes - Updated Android SDK to [19.5.0](https://github.com/urbanairship/android-library/releases/tag/19.5.0 - Updated iOS SDK to [19.1.2](https://github.com/urbanairship/ios-library/releases/tag/19.1.2 - Updated the native plugin hooks on Android: - Renamed the class `AirshipPluginForwardListeners` to `AirshipPluginExtenders` - Renamed `AirshipPluginForwardListeners.notificationListener` to `AirshipPluginExtenders.forwardNotificationListner` - Replaced `AirshipPluginForwardDelegates.deepLinkListener` with `AirshipPluginExtenders.onDeepLink` - Added `AirshipPluginExtenders.onShouldDisplayForegroundNotification` to allow overriding foreground notification display behavior - Updated the native plugin hooks on iOS: - Renamed the class `AirshipPluginForwardDelegates` to `AirshipPluginExtenders` - Renamed `AirshipPluginForwardDelegates.pushNotificationDelegate` to `AirshipPluginExtenders.pushNotificationForwardDelegate`. The delegate must now implement the protocol `AirshipPluginPushNotificationDelegate` which is the same as `PushNotificationDelegate` but without the `extendPresentationOptions` method. - Renamed `AirshipPluginForwardDelegates.registrationDelegate` to `AirshipPluginExtenders.registrationForwardDelegate` - Replaced `AirshipPluginForwardDelegates.deepLinkDelegate` with `AirshipPluginExtenders.onDeepLink` - Added `AirshipPluginExtenders.onWillPresentForegroundNotification` to allow overriding foreground notification display behavior ## 22.1.0 March 26, 2025 Minor release that updates the Android SDK to 19.4.0 and the iOS SDK to 19.1.1 and fixes a privacy manager bug. ### Changes - Updated Android SDK to [19.4.0](https://github.com/urbanairship/android-library/releases/tag/19.4.0) - Updated iOS SDK to [19.1.1](https://github.com/urbanairship/ios-library/releases/tag/19.1.1) - Fixed an issue where the Privacy Manager sent multiple opt-out requests after features were disabled following being enabled. ## 22.0.0 March 11, 2025 Major release that regenerates the plugin for React Native 0.78 and updates Android SDK. ### Changes - Update plugin to be compatible with React Native 0.78 - Updated Android SDK to [19.3.0](https://github.com/urbanairship/android-library/releases/tag/19.3.0) ## 21.2.0 February 25, 2025 Patch release that updates the Android SDK to 19.2.0 and the iOS SDK to 19.1.0. ### Changes - Updated Android SDK to [19.2.0](https://github.com/urbanairship/android-library/releases/tag/19.2.0) - Updated iOS SDK to [19.1.0](https://github.com/urbanairship/ios-library/releases/tag/19.1.0) ## 21.1.0 February 12, 2025 Patch release that updates the Android SDK to 19.1.0 and fixes the `messageUnreadCount` on the `MessageCenterUpdated` event. ### Changes - Updated Android SDK to [19.1.0](https://github.com/urbanairship/android-library/releases/tag/19.1.0) - Fixed MessageCenterUpdatedEvent.messageUnreadCount on iOS. ## 21.0.2 February 7, 2025 Patch release that updates the iOS SDK to 19.0.3 and fixes a Swift 5 warning. ### Changes - Updated iOS SDK to [19.0.3](https://github.com/urbanairship/ios-library/releases/tag/19.0.3) ## 21.0.1 February 3, 2025 Patch release that updates the iOS SDK to 19.0.2 ### Changes - Updated iOS SDK to [19.0.2](https://github.com/urbanairship/ios-library/releases/tag/19.0.2) ## 21.0.0 January 25, 2025 Major release that updates the native SDKs to 19.0.0. ### Changes - Updated Android SDK to [19.0.0](https://github.com/urbanairship/android-library/releases/tag/19.0.0). - Updated iOS SDK to [19.0.0](https://github.com/urbanairship/ios-library/releases/tag/19.0.0). - Xcode 16.2+ is required. - Updated min version to iOS 15+ & Android 23+. - Added manifest entry to disable the headless JS service when a background push is received before the module is loaded. This is not recommended to use unless its conflicting with a hybrid application. To disable the task, set the metadata entry to false for key `"com.urbanairship.reactnative.ALLOW_HEADLESS_JS_TASK_BEFORE_MODULE"`. ## 20.2.0 December 20, 2024 Minor release that updates to the latest Airship SDKs and adds new Feature Flag APIs. ### Changes - Updated Android SDK to [18.6.0](https://github.com/urbanairship/android-library/releases/tag/18.6.0). - Updated iOS SDK to [18.14.1](https://github.com/urbanairship/ios-library/releases/tag/18.14.1). - Adds `Airship.featureFlagManager.resultCache` to cache a result that can be used when `Airship.featureFlagManager.flag` fails to resolve a result. ## 20.1.0 December 6, 2024 Minor release that updates the Android Airship SDK to 18.5.0 and iOS Airship SDK to 18.13.0 ### Changes - Updated Android SDK to [18.5.0](https://github.com/urbanairship/android-library/releases/tag/18.5.0). - Updated iOS SDK to [18.13.0](https://github.com/urbanairship/ios-library/releases/tag/18.13.0). ## 20.0.4 November 27, 2024 Patch release that updates the iOS Airship SDK to 18.12.2 and Android Airship SDK to 18.4.2 ### Changes - Updated Android SDK to [18.4.2](https://github.com/urbanairship/android-library/releases/tag/18.4.2). - Updated iOS SDK to [18.12.2](https://github.com/urbanairship/ios-library/releases/tag/18.12.2). ## 20.0.3 November 22, 2024 Patch release that updates the Android build to no longer require the react-native path and updates the Airship Android SDK to 18.4.1. ### Changes - Updated Airship Android SDK to [18.4.1](https://github.com/urbanairship/android-library/releases/tag/18.4.1) - Updated Android build.gradle file to no longer look up the React Native version ## 20.0.2 November 8, 2024 Patch release that resolves an issue with Firebase integrations and fixes an issue with opt-in checks when requestAuthorizationToUseNotifications is set to false on iOS. ### Changes - Updated Airship iOS SDK to [18.12.1](https://github.com/urbanairship/ios-library/releases/tag/18.12.1) - Fixed issues caused by swizzling conflicts with some Firebase framework integrations. - Fixed opt-in check permissions querying when requestAuthorizationToUseNotifications is set to false on iOS. ## 20.0.1 November 7, 2024 Patch release that fixes a crash when using both Airship and `@react-native-firebase/messaging`. ### Changes - Updated Airship iOS SDK to [18.12.1](https://github.com/urbanairship/ios-library/releases/tag/18.12.1) - Updated Airship Android SDK to [18.4.0](https://github.com/urbanairship/android-library/releases/tag/18.4.0) ## 20.0.0 October 26, 2024 Major version that makes it easier to include Airship in a hybrid app. The only breaking change is when extending the `AirshipPluginExtender` protocol on Java there is a new `extendConfig(Context, AirshipConfigOptions.Builder)` method to implement. Applications that are not using `AirshipPluginExtender` or using Kotlin are not affected by the breaking change. ### Changes - Fixed tracking live activities started from a push notification - Added methods to plugin extenders to extend the Airship Config options - Exposed forward listeners on Android with `AirshipPluginForwardListeners` and delegates on iOS with `AirshipPluginForwardDelegates`. These listeners and delegates are useful for hybrid apps that need to listen for events both natively and in React Native context ## 19.4.2 October 23, 2024 Patch release to fix live activities and live updates on react old architecture and update Android and iOS SDK. ### Changes - Fixed live activities and live updates on react old architecture. - Updated Airship Android SDK to [18.3.3](https://github.com/urbanairship/android-library/releases/tag/18.3.3) - Updated Airship iOS SDK to [18.11.1](https://github.com/urbanairship/ios-library/releases/tag/18.11.1) ## 19.4.1 October 9, 2024 Patch release to fix a compile issue with the old Architecture on Android. ### Changes - Fixed compile issue when using old architecture on Android. ## 19.4.0 October 4, 2024 ### Changes - Updated Airship Android SDK to [18.3.2](https://github.com/urbanairship/android-library/releases/tag/18.3.2) - Updated Airship iOS SDK to [18.10.0](https://github.com/urbanairship/ios-library/releases/tag/18.10.0) - Added `notificationPermissionStatus` to `PushNotificationStatus` - Added options to `enableUserNotifications` to specify the `PromptPermissionFallback` when enabling user notifications - Added new `showMessageCenter(messageId?: string)` and `showMessageView(messageId: string)` to `MessageCenter` to display the OOTB UI even if `autoLaunchDefaultMessageCenter` is disabled - Added new APIs to manage [iOS Live Activities](https://docs.airship.com/platform/mobile/ios-live-activities/) - Added new APIs to manage [Android Live Updates](https://docs.airship.com/platform/mobile/android-live-updates/) - Added a new [iOS plugin extender](https://docs.airship.com/platform/mobile/setup/sdk/react-native/#ios-2) to modify the native Airship SDK after takeOff - Added new [Android plugin extender](https://docs.airship.com/platform/mobile/setup/sdk/react-native/#android-2) to modify the native Airship SDK after takeOff - Deprecated `com.urbanairship.reactnative.AirshipExtender` for the common `com.urbanairship.android.framework.proxy.AirshipPluginExtender`. The manifest name also changed from `com.urbanairship.reactnative.AIRSHIP_EXTENDER` to `com.urbanairship.plugin.extender` ## 19.3.2 September 13, 2024 Patch release to fix a compile issue with the new Architecture on iOS and to fix a potential race condition on the event listeners when refreshing the JS bridge. ### Changes - Fixed compile issue when using new architecture on iOS - Fixed potential race condition on events listeners when the JS bridge refreshes ## 19.3.1 September 5, 2024 Patch release to fix compile issue with 19.3.0 when using the old architecture on Android. ### Changes - Fix compile issue when using old architecture on Android ## 19.3.0 August 30, 2024 Minor release that adds early access support for Embedded Content. ### Changes - Adds AirshipEmbeddedView and listener methods to Airship.inApp for Embedded Content. - Exposes the Airship session ID on Airship.analytics. ## 19.2.1 August 23, 2024 Patch release that fixes an issue with extras parsing on notifications ### Changes - Allow JsonObject accept undefined values - Adds support for dynamic frameworks on iOS ## 19.2.0 August 13, 2024 Minor release that fixes test devices audience check, holdout group experiments displays and in-app experience displays when resuming from a paused state. Apps that use in-app experiences are encouraged to update. ### Changes - Updated Android SDK to 18.1.6. - Updated iOS SDK to 18.7.2. - Fixed test devices audience check. - Fixed holdout group experiments displays. - Fixed in-app experience displays when resuming from a paused state. ## 19.1.0 July 17, 2024 Minor release that fixes enabling or disabling all Airship features using `FEATURES_ALL` and adds possibility to enable and disable `Feature.FeatureFlags`. ### Changes - Fixed enabling or disabling features using `FEATURE_ALL`. - Added possibility to enable and disable `Feature.FeatureFlags` using the privacy manager. ## 19.0.0 July 9, 2024 Major release that updates the Android Airship SDK to 18. ### Changes - Updated iOS SDK to 18.5.0 - Updated Android SDK to 18.1.1 - Added iOS logPrivacyLevel that can be set in the environments when calling takeOff ## 18.0.5 June 21, 2024 Patch release to fix a regression on iOS with In-App Automations, Scenes, and Surveys ignoring screen, version, and custom event triggers. Apps using those triggers that are on 18.0.4 should update. ### Changes - Updated iOS SDK to 18.4.1 - Fixed regression with triggers ## 18.0.4 June 21, 2024 Patch release that updates iOS SDK to 18.4.0 and updates the airship mobile framework proxy to 6.3.0 which includes a fix for event management. ### Changes - Updated iOS SDK to 18.4.0 - Updated airship-mobile-framework-proxy to 6.3.0 - Fixed Event Emitter bug ## 18.0.3 May 17, 2024 Patch release that updates to latest iOS SDK. ### Changes - Updated iOS SDK to 18.2.2 [View Older Releases](https://github.com/urbanairship/react-native-airship/releases?q=created%3A%3C2024-05-15&expanded=true) # React Native Module Resources > API documentation, source code, and changelogs for the Airship React Native module. ## Platform Support {#platform-support} | Feature | iOS | Android | |----------------------------------------|-----|---------| | Push Notifications | ✅ | ✅ | | Live Activities | ✅ | ❌ | | Live Updates | ❌ | ✅ | | In-App Experiences | ✅ | ✅ | | Custom Views | ✅ | ✅ | | Embedded Content | ✅ | ✅ | | Message Center | ✅ | ✅ | | Preference Center | ✅ | ✅ | | Feature Flags | ✅ | ✅ | | Analytics | ✅ | ✅ | | Contacts | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ## API References * [React Native API Documentation](https://www.airship.com/docs/reference/libraries/react-native/latest/) ## GitHub Samples * [React Native Sample App](https://github.com/urbanairship/react-native-airship/tree/main/example) * [Expo Sample App](https://github.com/urbanairship/airship-expo-sample) ## Source * [Source](https://github.com/urbanairship/react-native-airship) ## Changelog * [React Native Changelog](https://www.airship.com/docs/developer/sdk-integration/react-native/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [React Native License](https://github.com/urbanairship/react-native-airship/blob/main/LICENSE) ### SDK Installation Complete installation and configuration guides for the Airship React Native module. # Install and Set Up the React Native Module > How to install the Airship React Native module. The Airship React Native module provides a TypeScript-first interface for React Native apps. It wraps the native iOS and Android Airship SDKs, giving you full access to all platform features while maintaining a JavaScript/TypeScript developer experience with strong typing and modern async/await patterns. ## Requirements For supported versions and React Native compatibility, see [Supported Versions](https://github.com/urbanairship/react-native-airship?tab=readme-ov-file#supported-versions). ### iOS * Xcode `15.3+` * minimum deployment target iOS `15+` ### Android * minSdkVersion `23+` * compileSdkVersion `36+` ## Standard React Native Setup Install the plugin using yarn or npm: `yarn add @ua/react-native-airship` ## Expo Setup Apps using Expo's managed workflows can use the `airship-expo-plugin` to configure the project. `expo install airship-expo-plugin` `yarn add @ua/react-native-airship` ### Configure the plugin Add the plugin to the `app.json` with the app's config: ```js "plugins":[ [ "airship-expo-plugin", { "android":{ "icon": "./assets/ic_notification.png", "customNotificationChannels": "./assets/notification_channels.xml", "airshipExtender": "./assets/AirshipExtender.kt" }, "ios":{ "mode": "development", "notificationService": "./assets/NotificationService.swift", "notificationServiceInfo": "./assets/NotificationServiceExtension-Info.plist", "notificationServiceTargetName": "NotificationServiceExtension", "developmentTeamID": "MY_TEAM_ID", "deploymentTarget": "15", "airshipExtender": "./assets/AirshipPluginExtender.swift" } } ] ] ``` Android Config: - icon: Required. Local path to an image to use as the icon for push notifications. 96x96 all-white png with transparency. The name of the icon will be the resource name. - customNotificationChannels: Optional. The local path to a Custom Notification Channels resource file. - airshipExtender: Optional. The local path to a AirshipExtender.kt file. iOS Config: - mode: Required. The APNS entitlement. Either `development` or `production`. - notificationService: Optional. The local path to a custom Notification Service Extension or `DEFAULT_AIRSHIP_SERVICE_EXTENSION` for Airship's default one. - notificationServiceInfo: Optional. Airship will use a default one if not provided. The local path to a Notification Service Extension Info.plist. - notificationServiceTargetName: Optional. Defaults to NotificationServiceExtension if not provided. - developmentTeamID: Optional. The Apple Development Team ID used to configure the Notification Service Extension target. - deploymentTarget: Optional. The minimum Deployment Target version used to configure the Notification Service Extension target. Defaults to iOS 15. - airshipExtender: Optional. The local path to a AirshipPluginExtender.swift file. ## Calling TakeOff `takeOff` should be called in a standard application at the beginning of the lifecycle. Once `takeOff` is called, the config will be stored and applied for future app inits. If `takeOff` is called again with a different config, the new config will not be applied until the next app init. ```ts import Airship from '@ua/react-native-airship'; Airship.takeOff({ default: { appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" }, site: "us", // use "eu" for EU cloud projects urlAllowList: ["*"], android: { notificationConfig: { icon: "ic_notification", accentColor: "#00ff00" } } }); ``` For a complete list of configuration options, see the [AirshipConfig reference](https://www.airship.com/docs/reference/libraries/react-native/latest/interfaces/AirshipConfig.html). ## Test the integration After completing the setup, verify your integration: 1. **Build and run your app** on your iOS or Android device/simulator/emulator. 2. **Check the console output** for Airship channel creation: - **iOS**: Look for a log message in Xcode console: `Channel ID: ` - **Android**: Look for a log message in logcat: `Airship channel created: ` - The channel ID confirms successful SDK initialization. - For more detailed logging, see [Logging](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/logging/). If you see the channel ID in the console and no errors, your integration is successful. ## Next steps - [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/advanced-configuration/): Configure URL allowlists and other advanced settings - [Extending Airship](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/extending-airship/): Access native SDK features for advanced customization - [Push Notifications](https://www.airship.com/docs/developer/sdk-integration/react-native/push-notifications/getting-started/): Configure push notifications - [Deep Links](https://www.airship.com/docs/developer/sdk-integration/react-native/deep-links/): Handle deep links in your app If you don't see a channel ID or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/react-native/troubleshooting/initialization/) for common problems and solutions. # Logging > Configure log levels and privacy settings to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. By default, the log level is set to **Info** for development builds and **Error** for production builds to ensure clean logs in a live environment. ## Log levels The following log levels are available, ordered from most to least verbose. | Log Level | Description | | :-------- | :---------- | | **Verbose** | Reports highly detailed SDK status, which is useful for deep debugging and troubleshooting. | | **Debug** | Reports general SDK status with more detailed information than `Info`. | | **Info** | Reports general SDK status and lifecycle events. | | **Warning** | Used for API deprecations, invalid setup, and other potentially problematic situations that are generally recoverable. | | **Error** | Used for critical errors, exceptions, and other situations that the SDK cannot gracefully handle. | | **None** | Disables all logging. | ## Configuring log levels You can set the log level in the Airship config options that are passed during takeOff. This setting acts as a minimum, and only logs at that level and higher will be logged. ```typescript // Available log levels: verbose, debug, info, warning, none await Airship.takeOff({ default: { logLevel: "verbose" }, ... }); ``` > **Note:** Due to how takeOff caches config, you may need to restart the app after the new takeOff before the log levels take effect. ## Log privacy levels For better security in production environments, you can control the visibility of log contents using privacy levels. This is especially useful when you need to debug a release build without exposing sensitive information. ### private (default) This is the default setting, designed to protect data in a production environment. * **iOS**: Uses `os.Logger` to log all messages at the `private` level. The content of most logs will be redacted and will not be visible in the Console app unless a special profile is installed on the device. * **Android**: Uses the standard `android.util.Log`. In production builds, `verbose` and `debug` messages are completely dropped and will not be logged. ### public This setting increases log visibility, making it easier to capture detailed information from release builds. * **iOS**: All logs are sent to `os.Logger` with a `public` privacy level, preventing their content from being redacted. * **Android & iOS**: To ensure visibility in production builds, `verbose` and `debug` messages are automatically elevated to the `info` log level. ## Setting the privacy level You can set the privacy level in the Airship config options that are passed during takeOff. ```typescript await Airship.takeOff({ default: { logLevel: "verbose", ios: { logPrivacyLevel: "public" }, android: { logPrivacyLevel: "public" } }, ... }); ``` > **Note:** Due to how takeOff caches config, you may need to restart the app after the new takeOff before the log levels take effect. # Locale > Configure locale behavior and override the default locale that Airship uses. The Airship SDK is localized in 48 different languages for all strings included within the SDK. The strings within the app will automatically use the device's or app's configured [Locale](https://www.airship.com/docs/reference/glossary/#locale). Airship uses the user's locale for various locale-sensitive tasks, including selecting the language for messages and reporting for analytics. Apps can override the locale so that Airship uses a different locale than the device's current locale for messaging. ## Overriding the locale You can override the locale programmatically at runtime, which takes precedence over the device's locale settings. ```typescript await Airship.locale.setLocaleOverride("de"); ``` ## Clearing the locale override To remove a locale override and return to using the device's locale: ```typescript await Airship.locale.clearLocaleOverride(); ``` ## Getting the current locale To retrieve the locale that Airship is currently using: ```typescript const locale = await Airship.locale.getCurrentLocale(); ``` # Advanced Configuration > Configure advanced settings like URL allowlists and other options for the Airship React Native module. ## URL Allowlist The URL allowlist controls which URLs the Airship SDK is able to act on. Configure the URL allowlist in your `takeOff` config options using the following properties: - `urlAllowListScopeOpenUrl`: Only URLs allowed for this scope can be opened from an action, displayed in landing page, displayed in an HTML in-app message, or displayed as media in an In-App Automation. Defaults to any Airship-originated URLs and YouTube URLs. - `urlAllowListScopeJavaScriptInterface`: These URLs are checked before the Airship JavaScript interface is injected into the webview. Defaults to any Airship-originated URLs. - `urlAllowList`: Both scopes are applied to these URLs. ```ts Airship.takeOff({ default: { appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" }, site: "us", urlAllowList: ["*"], // Accept all URLs // Or configure specific scopes: urlAllowListScopeOpenUrl: ["https://example.com/*", "https://*.youtube.com/*"], urlAllowListScopeJavaScriptInterface: ["https://example.com/*"] }); ``` **Valid URL pattern syntax** ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` To accept any URL within the SDK, set the `urlAllowList` to `["*"]`. For a complete list of configuration options, see the [AirshipConfig reference](https://www.airship.com/docs/reference/libraries/react-native/latest/interfaces/AirshipConfig.html). # Extend Airship > How to extend the Airship React Native module to access native iOS and Android SDK features not exposed through the React Native API. You can provide a plugin extender that will be automatically loaded for the app. The extender can be used to modify the Airship config before SDK initialization and to access the underlying native SDK once Airship is ready. This gives the app a chance to customize parts of Airship that are not configurable through React Native, such as setting up [iOS Live Activities](https://www.airship.com/docs/developer/sdk-integration/react-native/live-activities/) and [Android Live Updates](https://www.airship.com/docs/developer/sdk-integration/react-native/live-updates/). ## iOS For iOS, create a Swift file named `AirshipPluginExtender.swift` and needs to be included in the main app target. Make sure the class has the `@objc(AirshipPluginExtender)` annotation and inherits `AirshipPluginExtenderProtocol`. ```swift import Foundation import AirshipKit import AirshipFrameworkProxy import ActivityKit @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { public static func onAirshipReady() { // Called when Airship is ready on the MainActor } public static func extendConfig(config: inout AirshipConfig) { // Called to extend the AirshipConfig before SDK initialization } } ``` ## Android Create a file in the App's src directory named `AirshipExtender`. It needs to extend `com.urbanairship.android.framework.proxy.AirshipPluginExtender` and have an empty constructor. ```kotlin // Replace with your package package com.example import android.content.Context import androidx.annotation.Keep import com.urbanairship.AirshipConfigOptions import com.urbanairship.UAirship import com.urbanairship.android.framework.proxy.AirshipPluginExtender @Keep public final class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { // Called when Airship is ready on a background thread. // Avoid doing long running, blocking work or it will delay Airship } override fun extendConfig( context: Context, configBuilder: AirshipConfigOptions.Builder ): AirshipConfigOptions.Builder { // Called to extend the AirshipConfig before SDK initialization return configBuilder } } ``` Register the extender in the manifest: ```xml ``` ### Push Notifications Configure and implement push notifications for iOS and Android platforms. # Push Notifications > How to configure your application to receive and respond to notifications. Before setting up push notifications in your app, you need to configure push services in the Airship dashboard. See [iOS Channel Configuration](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration) for APNs setup and [Android Channel Configuration](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#android-channel-configuration) for FCM setup. ## Platform Setup Before you can send and receive push notifications, you need to configure your app for the platform(s) you're targeting. ### iOS #### Standard React Native #### Enable Push Notifications Capability 1. Open your project in Xcode. 2. Click on your project in the Project Navigator. 3. Select your main app target and then click the **Signing & Capabilities** tab. 4. If you do not see Push Notifications enabled, click **+ Capability** and add **Push Notifications**. ![Adding the Push Notifications capability in Xcode](https://www.airship.com/docs/images/ios-enable-push-notifications-capabilities_hu_2e1789fffb02612b.webp) *Adding the Push Notifications capability in Xcode* #### Enable Background Modes 1. Select your main app target and then click the **Signing & Capabilities** tab. 2. Click **+ Capability** and add **Background Modes**. ![Adding the Background Modes capability in Xcode](https://www.airship.com/docs/images/ios-enable-background-mode-capabilities_hu_f135d9fec0ba0d06.webp) *Adding the Background Modes capability in Xcode* 3. In the **Background Modes** section, select the **Remote notifications** checkbox. ![Enabling Remote notifications in Background Modes](https://www.airship.com/docs/images/ios-background-mode-remote-notifications_hu_7e38b08288fcd7b2.webp) *Enabling Remote notifications in Background Modes* #### Notification Service Extension

To take advantage of notification attachments, such as images, animated gifs, and video, you will need to create a notification service extension.

Follow the steps in the [iOS Notification Service Extension Guide](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/). #### Expo The Airship Expo plugin automatically configures iOS capabilities (Push Notifications and Background Modes) and the Notification Service Extension. No manual Xcode configuration is required. ### Android Configure Firebase Cloud Messaging (FCM) to enable push notifications on Android. #### FCM Setup #### Standard React Native 1. Download the Android Firebase configuration file `google-services.json` from the application's Firebase console into the root directory at `android/app/google-services.json`. If your react-native application does not have an associated app in the Firebase console, follow the [FCM setup instructions](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#fcm-setup) to set one up. #### Expo Expo automatically handles the `google-services.json` file through its build process. See the [Expo documentation](https://docs.expo.dev/push-notifications/fcm-credentials/) for details on configuring FCM credentials. #### HMS Setup 1. Follow [Huawei's documentation](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-integrating-sdk-0000001050040084) to set up the HMS SDK. > **Note:** Airship requires HMS Core Push SDK 6.3.0.304 or newer. 2. Add `airshipHmsEnabled=true` to the app's gradle.properties. #### Notification Configuration Configure the notification icon and accent color in your `takeOff` config: ```ts Airship.takeOff({ default: { appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" }, site: "us", android: { notificationConfig: { icon: "ic_notification", accentColor: "#00ff00" } } }); ``` See the [React Native Setup guide](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/getting-started/) for complete `takeOff` configuration options. ## Enable User Notifications The Airship SDK distinguishes between *user notifications* (visible to users) and *silent push notifications* (background data delivery). User notifications require explicit permission from the user. By default, user notifications are disabled. Enable them when you want to show visible notifications to users. ### Basic Enablement The simplest way to enable user notifications is with `setUserNotificationsEnabled()`: ```typescript await Airship.push.setUserNotificationsEnabled(true); ``` This will prompt the user for permission if not already granted. However, it does not provide feedback on whether the user accepted or denied the permission. > **Note:** For apps that target Android 13 (API 33) and above, enabling user notifications will display a runtime permission prompt. > > To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately on app startup, and instead wait for a more appropriate time to prompt for notification permission. ### Async Enablement with Fallback For more control over the permission flow, use `enableUserNotifications()` which returns the permission result and supports a fallback option: ```typescript // Enable with system settings fallback const granted = await Airship.push.enableUserNotifications({ fallback: PromptPermissionFallback.SystemSettings }); if (granted) { console.log('Notifications enabled'); } else { console.log('Notifications denied'); } ``` The `SystemSettings` fallback option will prompt the user to open system settings if permission is denied on iOS or denied silently on Android. This gives users a second chance to enable notifications if they initially declined. ### Checking Notification Status To check if user notifications are currently enabled: ```typescript const enabled = await Airship.push.isUserNotificationsEnabled(); ``` For more detailed status information, use `getNotificationStatus()`: ```typescript const status = await Airship.push.getNotificationStatus(); console.log('Are notifications allowed:', status.areNotificationsAllowed); console.log('Is opted in:', status.isOptedIn); ``` To monitor notification status changes in real-time: ```typescript Airship.addListener(EventType.PushNotificationStatusChangedStatus, (event) => { console.log('Notification status changed:', event.status); console.log('Is opted in:', event.status.isOptedIn); }); ``` ### Getting the Registration Token To get the platform-specific push token (APNs token on iOS, FCM token on Android): ```typescript const token = await Airship.push.getRegistrationToken(); ``` For more advanced event handling and token updates, see [Handling Notification Events](https://www.airship.com/docs/developer/sdk-integration/react-native/push-notifications/handling-notification-events/). If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/react-native/troubleshooting/push-notifications/) to check notification status and fix common issues. # Notification Events > Listen for push notification events, handle user responses, and manage active notifications. The Airship SDK provides event listeners for when a push is received or a notification is interacted with. Apps can use these events for custom push processing. ## Notification Events The SDK provides several event listeners for handling push notifications at different stages. ### Push Received Listen for when a push notification is received: ```typescript Airship.addListener(EventType.PushReceived, (event) => { console.log('Push received:', event.pushPayload); // Handle push received event }); ``` This event fires when a push notification arrives, regardless of whether the app is in the foreground or background. ### Notification Response Listen for when a user interacts with a notification: ```typescript Airship.addListener(EventType.NotificationResponse, (response) => { console.log('Notification tapped:', response); console.log('Action ID:', response.actionId); // Handle the notification response if (response.actionId === 'custom_action') { // Handle custom action } }); ``` This event fires when a user taps on a notification or a notification action button. ### Registration Token Updates Listen for when the push registration token is generated or updated: ```typescript Airship.addListener(EventType.PushTokenReceived, (event) => { console.log('Push token:', event.pushToken); // Send token to your backend if needed }); ``` You can also retrieve the current token at any time: ```typescript const token = await Airship.push.getRegistrationToken(); ``` ### Notification Status Changes Monitor changes to notification permission status: ```typescript Airship.addListener(EventType.PushNotificationStatusChangedStatus, (event) => { console.log('Notification status changed:', event.status); console.log('Is opted in:', event.status.isOptedIn); }); ``` ## Managing Active Notifications You can retrieve and clear notifications that are currently displayed in the notification center. ### Get Active Notifications Retrieve the list of currently displayed notifications: ```typescript const notifications = await Airship.push.getActiveNotifications(); console.log('Active notifications:', notifications); ``` > **Note:** On Android, this list only includes notifications sent through Airship. ### Clear Notifications Clear all notifications for the app: ```typescript Airship.push.clearNotifications(); ``` Clear a specific notification by identifier: ```typescript Airship.push.clearNotification(identifier); ``` > **Note:** On Android, you can use this method to clear notifications outside of Airship. The identifier is in the format `:`. ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. ### Platform Configuration **iOS**: Set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). **Android**: All push messages are delivered in the background. By default, Airship will treat messages without an `alert` as silent. > **Note:** Silent pushes do not have guaranteed delivery. Factors affecting delivery include battery life, WiFi connectivity, and the number of silent pushes sent within a recent time period. These metrics are determined solely by iOS/Android and APNs/FCM. > > Silent push is best used for supplementing regular app behavior rather than providing critical functionality. For example, an app could use a silent push to pre-fetch new data ahead of time to reduce load times when the app is later launched by the user. ### Handling Silent Notifications Silent notifications will trigger the `PushReceived` event but will not display a notification to the user: ```typescript Airship.addListener(EventType.PushReceived, (event) => { if (!event.pushPayload.alert) { console.log('Silent push received'); // Perform background work } }); ``` # Customize Notifications > Configure notification options, foreground presentation, badges, and custom categories. ## iOS Notification Options By default, the Airship SDK will request `Alert`, `Badge`, and `Sound` notification options for remote notifications. This can be configured by setting notification options before enabling user notifications. ```typescript await Airship.push.iOS.setNotificationOptions([ iOS.NotificationOption.Alert, iOS.NotificationOption.Badge, iOS.NotificationOption.Sound, ]); ``` ### Provisional Authorization Apps can request provisional authorization along with the usual notification options. When requesting provisional authorization apps do not need to prompt the user for permission initially, and notifications will be delivered in a non-interruptive manner to the Notification Center until the user explicitly chooses to keep delivering messages either prominently or quietly. ```typescript await Airship.push.iOS.setNotificationOptions([ iOS.NotificationOption.Alert, iOS.NotificationOption.Badge, iOS.NotificationOption.Sound, iOS.NotificationOption.Provisional, ]); ``` ### Foreground Presentation Options When a push is received in the foreground on iOS, how the notification is displayed to the user is controlled by foreground presentation options. By default, the SDK will not set any options so the notification will be silenced. #### Global Foreground Presentation Options Set default foreground presentation options for all notifications: ```typescript await Airship.push.iOS.setForegroundPresentationOptions([ iOS.ForegroundPresentationOption.List, iOS.ForegroundPresentationOption.Badge, iOS.ForegroundPresentationOption.Sound, ]); ``` #### Per-Notification Foreground Presentation Options For more control, you can override presentation options on a per-notification basis using a callback: ```typescript Airship.push.iOS.setForegroundPresentationOptionsCallback(async (pushPayload) => { // Check the push payload and return custom options if (pushPayload.extras?.highPriority) { // Show high priority notifications with all options return [ iOS.ForegroundPresentationOption.List, iOS.ForegroundPresentationOption.Banner, iOS.ForegroundPresentationOption.Sound, iOS.ForegroundPresentationOption.Badge ]; } // Return null to use the default options return null; }); ``` The callback receives the full push payload and should return quickly to avoid delaying notification delivery. Returning `null` uses the default options set with `setForegroundPresentationOptions()`. ### Badges The badge on iOS presents a counter on top of the application icon. You can control this directly through Airship. ```typescript // Set badge number await Airship.push.iOS.setBadgeNumber(20); // Reset badge await Airship.push.iOS.setBadgeNumber(0); // Enable auto-badge await Airship.push.iOS.setAutobadgeEnabled(true); ``` > **Important:** When using auto-badge, only modify the badge value through Airship methods to ensure the value stays in sync. ### Quiet Time Quiet time allows you to suppress notifications during specific hours. Notifications are still received but won't be displayed to the user during the quiet time window. > **Note:** Quiet time is only supported on iOS. ## Android Notification Configuration You can configure Android-specific notification behaviors and settings. ### Foreground Display Control By default, push notifications received while the app is in the foreground on Android are displayed to the user. You can control this behavior on a per-notification basis using a predicate: ```typescript Airship.push.android.setForegroundDisplayPredicate(async (pushPayload) => { // Check the push payload and return true to display, false to suppress if (pushPayload.extras?.silent) { return false; // Don't display notifications marked as silent } // Display all other notifications return true; }); ``` The predicate receives the full push payload and should return quickly to avoid delaying notification delivery. Return `true` to display the notification or `false` to suppress it. ## Adding Custom Notification Categories The Airship module supports adding custom notification categories on iOS, and custom notification action button groups on Android, by way of a specially named plist and XML file, respectively. Once these files are added to your iOS and Android projects, the module will load this file at runtime and automatically register your custom categories with the Airship API. ### iOS Setup Add a new plist file to your app's Xcode project, named `UACustomNotificationCategories.plist`, and make sure to add the file to your application's target. The structure of the plist file follows the format used by the core Airship SDK for its default categories. As a starting point, see the example below. Note that key and string values prefixed with **"ua_"** are reserved by the Airship SDK. **UACustomNotificationCategories.plist** ```xml yes_no_background authenticationRequired foreground identifier yes title Yes title_resource notification_button_yes authenticationRequired foreground identifier no title No title_resource notification_button_no yes_no_foreground foreground identifier yes title Yes title_resource notification_button_yes authenticationRequired foreground identifier no title No title_resource notification_button_no ``` ### Android Setup First, add a new xml file to your app's `main/res/xml` directory, named `ua_custom_notification_buttons.xml`. As with the iOS example above, the structure of this XML file follows the format used by the core Airship SDK for its default categories. And as before, entities and resource names with the **"ua_"** prefix are reserved by the Airship SDK. A similar example is given below. The icon and label resources shown are only examples; to create custom categories of your own, you should either use built-in Android resources or supply resources defined by your app. **ua_custom_notification_buttons.xml** ```xml ``` ## Adding Custom Notification Channels (Android) The Airship module also supports adding custom notification channels on Android, by supplying a specially named XML file. First, add a new xml file to your app's `main/res/xml` directory, named `ua_custom_notification_channels.xml`. The structure of this XML file follows the format used by the core Airship SDK for its default notification channels. A simple example is shown below. Note that entities and resource names with the **"ua_"** prefix are reserved by the Airship SDK. To create custom notification channels of your own, you should either use built-in Android resources or supply resources defined by your app. **ua_custom_notification_channels.xml** ```xml ``` ### In-App Experiences Configure and control In-App Experiences in React Native applications. # In-App Experiences > Pause, resume, and control display timing for In-App Experiences. In-App Experiences are automatically enabled when you integrate the Airship SDK. Use these methods to control when and how they are displayed. ## Pausing and Resuming Display You can pause and resume In-App Experiences to control when they are displayed to users. ```typescript // Pause in-app experiences await Airship.inApp.setPaused(true) // Resume in-app experiences await Airship.inApp.setPaused(false) // Check if paused const isPaused = await Airship.inApp.isPaused() ``` ### Auto-Pause on Launch You can configure the SDK to automatically pause In-App Experiences on launch. This is useful if you want to defer showing In-App Experiences until after onboarding or other critical app flows. ```typescript await Airship.takeOff({ production: { appKey: "", appSecret: "" }, development: { appKey: "", appSecret: "" }, inProduction: true, site: "us", autoPauseInAppAutomationOnLaunch: true }) ``` See the [React Native Plugin Setup guide](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/getting-started/) for complete `takeOff` configuration options. When you're ready to display In-App Experiences, call `setPaused(false)`: ```typescript await Airship.inApp.setPaused(false) ``` ## Display Interval Control the minimum time between In-App Experience displays to avoid overwhelming users. ```typescript // Set display interval to 5 seconds await Airship.inApp.setDisplayInterval(5000) // Get current display interval const interval = await Airship.inApp.getDisplayInterval() ``` The display interval is the minimum time (in milliseconds) that must pass between displaying In-App Experiences. # Embedded Content > Integrate Embedded Content into your React Native app to display Scene content directly within your app's screens. For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see [Embedded Content]({{< ref "/guides/features/messaging/scenes/embedded-content.md" >}}). ## Adding an embedded view The `AirshipEmbeddedView` component defines a place for Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `embeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```typescript import { AirshipEmbeddedView } from '@ua/react-native-airship' // Show any "home_banner" Embedded Content ``` ## Sizing The `AirshipEmbeddedView` accepts an optional `style` prop for controlling its dimensions and layout. You can use standard React Native `ViewStyle` properties to set width, height, and other layout constraints. **Custom sizing** ```typescript ``` ## Checking if embedded content is ready Use `Airship.inApp.isEmbeddedReady` to check if embedded content is currently available for a given ID: ```typescript import Airship from '@ua/react-native-airship' const isReady = Airship.inApp.isEmbeddedReady("home_banner") ``` ## Listening for embedded content updates Use `Airship.inApp.addEmbeddedReadyListener` to listen for changes in the availability of embedded content for a given ID. The listener receives a boolean indicating whether content is ready to display. The listener is called immediately with the current state when first added, and then again whenever the state changes. **Embedded ready listener** ```typescript import Airship from '@ua/react-native-airship' const subscription = Airship.inApp.addEmbeddedReadyListener("home_banner", (isReady) => { console.log("home_banner is ready:", isReady) }) // Remove the listener when no longer needed subscription.remove() ``` ## Showing a placeholder when content is unavailable The `AirshipEmbeddedView` renders nothing when no content is available. Use `addEmbeddedReadyListener` to toggle between a placeholder and the embedded view based on content availability. **Embedded view with placeholder** ```typescript import React, { useState, useEffect } from 'react' import { View, Text, StyleSheet } from 'react-native' import Airship, { AirshipEmbeddedView } from '@ua/react-native-airship' function HomeBanner() { const [isReady, setIsReady] = useState( Airship.inApp.isEmbeddedReady("home_banner") ) useEffect(() => { const subscription = Airship.inApp.addEmbeddedReadyListener( "home_banner", (ready) => { setIsReady(ready) } ) return () => { subscription.remove() } }, []) if (!isReady) { return ( No content available ) } return ( ) } const styles = StyleSheet.create({ placeholder: { width: '100%', height: 200, justifyContent: 'center', alignItems: 'center', }, banner: { width: '100%', minHeight: 200, }, }) ``` # Custom Views > Register custom native views to use within Scenes. A *Custom View* is a native view from your mobile or web application embedded into a Scene. Custom Views can display any native content your app exposes, so you can reuse that existing content within any screen in a Scene. Custom Views allow you to embed native iOS and Android views within Scenes, giving you full control over design and layout while leveraging Airship's targeting and orchestration capabilities. ## Requirements To use Custom Views in React Native, you must extend the native Airship modules using the `AirshipPluginExtender`. See [Extending Airship](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/extending-airship/) for setup instructions. ## Registering Custom Views Custom Views must be registered on each native platform separately: ### iOS See the [Apple Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/custom-views/) for detailed implementation instructions. Custom Views should be registered after takeOff in your native iOS code: ```swift import AirshipKit @objc(AirshipExtender) class AirshipExtender: NSObject, AirshipPluginExtenderDelegate { func onAirshipReady() { // Register custom views AirshipCustomViewManager.shared.register(name: "my-custom-view") { args in // Return your SwiftUI view MyCustomView(args: args) } } } ``` ### Android See the [Android Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/custom-views/) for detailed implementation instructions. Custom Views should be registered during the `onAirshipReady` callback in your native Android code: ```kotlin import com.urbanairship.AirshipCustomViewManager class AirshipExtender : AirshipPluginExtender { override fun onAirshipReady(context: Context) { // Register custom views AirshipCustomViewManager.register("my-custom-view") { context, args -> // Return your Android View MyCustomView(context, args) } } } ``` ## Using Custom Views Once registered, Custom Views can be added to Scenes in the Airship dashboard: 1. Create or edit a Scene 2. Add the **Custom View** content element to a screen 3. Enter the view name (e.g., `my-custom-view`) that matches the name you registered in your native code 4. Optionally add key-value pairs to pass custom properties to the view The native view will be displayed within the Scene with the properties you configured. ### Message Center Implement Message Center to provide an inbox for rich HTML-based messages. # Message Center > The default Message Center is available for React Native with minimal integration required. Basic theming options are supported. Message Center provides an inbox for rich, HTML-based messages. Learn more about Message Center in our [feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Display the Message Center Display the Message Center with a single method call: ```ts await Airship.messageCenter.display(); ``` To customize the Message Center UI or navigation, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/react-native/message-center/embedding/). ## Fetch Messages Retrieve messages from the inbox: ```ts const messages = await Airship.messageCenter.getMessages(); ``` ## Listen for Message Updates Subscribe to message updates using event listeners: ```ts Airship.addListener(EventType.MessageCenterUpdated, () => { // Update your UI Airship.messageCenter.getMessages().then((messages) => { // Handle messages }); }); ``` ## Listen for Unread Count Changes Subscribe to unread count updates: ```ts const unreadCount = await Airship.messageCenter.getUnreadCount(); // Update badge or UI ``` ## Refresh Messages Manually refresh the message list from the server: ```ts try { await Airship.messageCenter.refreshMessages(); } catch (error) { console.log('Unable to refresh inbox: ', error); } ``` ## Mark Messages as Read Mark one or more messages as read: ```ts try { await Airship.messageCenter.markMessageRead('message-id'); } catch (error) { console.log('Unable to mark message as read: ', error); } ``` ## Delete Messages Delete one or more messages: ```ts try { await Airship.messageCenter.deleteMessage('message-id'); } catch (error) { console.log('Unable to delete message: ', error); } ``` # Embed the Message Center > Airship's SDK provides a simple interface for managing the Message Center within your application. This guide covers creating custom Message Center implementations for React Native applications. ## Override Default Display Behavior To use a custom Message Center implementation instead of the default UI, disable auto-launch and add a listener to handle display events: ```ts // Disable the default UI Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false); // Add a listener to handle display events Airship.addListener(EventType.DisplayMessageCenter, (event) => { if (event.messageId) { // Navigate to specific message } else { // Navigate to message center } }); ``` ## Custom Message Center Implementation For complete control over Message Center placement and navigation, create a custom implementation using the Message Center components. ### Using the MessageView The `MessageView` component can be used to display individual messages: ```ts ``` ### Example: Custom Message Center Inbox Here's an example of a complete Message Center inbox implementation: ```ts import * as React from 'react'; import { Text, View, FlatList, TouchableHighlight, RefreshControl, } from 'react-native'; import Moment from 'moment'; import Airship, { Subscription, EventType, InboxMessage } from '@ua/react-native-airship'; interface MessageCenterScreenProps { navigation: any; } function Item({ message, navigation }: { message: any; navigation: any }) { return ( navigation.navigate('MessageDetails', { messageId: message.id, title: message.title, }) } > {message.title} {Moment(message.sentDate).format('MM/DD/YYYY')} ); } export default class MessageCenterScreen extends React.Component< MessageCenterScreenProps, { messages: InboxMessage[]; refreshing: boolean; } > { private updateSubscription?: Subscription; constructor(props: MessageCenterScreenProps) { super(props); this.state = { messages: [], refreshing: true, }; this.refreshMessageCenter = this.refreshMessageCenter.bind(this); this.handleUpdateMessageList = this.handleUpdateMessageList.bind(this); } componentDidMount(): void { this.updateSubscription = Airship.addListener( EventType.MessageCenterUpdated, this.handleUpdateMessageList ); this.handleUpdateMessageList(); } componentWillUnmount(): void { this.updateSubscription?.remove(); } handleUpdateMessageList() { Airship.messageCenter.getMessages().then((data) => { this.setState({ messages: data, refreshing: false, }); }); } refreshMessageCenter() { Airship.messageCenter .refreshMessages() .then(() => { this.setState({ refreshing: false, }); }) .catch((error) => { console.log('failed to refresh', error); }); } render() { return ( ( )} keyExtractor={(item) => item.id} refreshControl={ } /> ); } } ``` ### Example: Message Detail Screen Here's an example of a message detail screen: ```ts import * as React from 'react'; import { View, ActivityIndicator, Alert } from 'react-native'; import { MessageView } from '@ua/react-native-airship'; interface MessageScreenProps { navigation: any; route: any; } export default class MessageScreen extends React.Component< MessageScreenProps, { animating: boolean; } > { constructor(props: MessageScreenProps) { super(props); this.state = { animating: true, }; this.startLoading = this.startLoading.bind(this); this.finishLoading = this.finishLoading.bind(this); this.failedLoading = this.failedLoading.bind(this); } startLoading() { this.setState({ animating: true }); } finishLoading() { this.setState({ animating: false }); } failedLoading() { this.setState({ animating: false }); Alert.alert('Error', 'Unable to load message. Please try again later', [ { text: 'OK', onPress: () => this.props.navigation.goBack() }, ]); } render() { const { params } = this.props.route; const messageId = params ? params.messageId : ''; return ( {this.state.animating && ( )} ); } } ``` For more examples, see our [sample app](https://github.com/urbanairship/react-native-module/tree/main/example). ### Preference Center Implement Preference Center to let users control their subscription preferences. # Preference Center > Preference Center allows users to opt in and out of subscription lists configured via the Airship Dashboard. > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. Preference Center provides a pre-built UI for users to manage their subscription preferences. Learn more in the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Display a Preference Center Display a Preference Center with a single method call: ```ts await Airship.preferenceCenter.display("preference-center-id"); ``` To build a custom Preference Center UI, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/react-native/preference-center/embedding/). # Embed the Preference Center > Create custom Preference Center UIs by fetching the config and building your own subscription management interface. This guide covers creating custom Preference Center UIs for React Native applications. Unlike the default Preference Center, you'll build your own UI from scratch using the Preference Center configuration and subscription list APIs. ## Override Default Display Behavior To use a custom Preference Center instead of the default UI, disable auto-launch for the specific Preference Center ID and handle display events: ```ts // Disable the OOTB UI for this Preference Center Airship.preferenceCenter.setAutoLaunchDefaultPreferenceCenter( "preference-center-id", false ); // Add a listener to handle display events Airship.addListener(EventType.DisplayPreferenceCenter, (event) => { const preferenceCenterId = event.preferenceCenterId; // Navigate to your custom preference center UI navigateToCustomPreferenceCenter(preferenceCenterId); }); ``` ## Fetching Preference Center Config The Preference Center config contains all the information needed to build your UI, including subscription lists, sections, and display settings. ```ts const config = await Airship.preferenceCenter.getConfig("preference-center-id"); ``` > **Note:** The config might not be available immediately on first app start. Implement exponential backoff if automatically retrying, or provide a UI for users to manually retry. ## Building Your Custom UI You'll need to: 1. **Fetch the config** to get the list of subscription lists and their current state 2. **Build your UI** using the config data (sections, subscription lists, display settings) 3. **Update subscription lists** when users make changes using the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/react-native/audience/subscription-lists/) ### Example Implementation See a complete example in our [React Native sample app](https://github.com/urbanairship/react-native-airship/blob/main/example/src/screens/PreferenceCenterScreen.tsx). ```ts import React, { useEffect, useState } from 'react'; import { View, Text, Switch } from 'react-native'; import Airship from '@ua/react-native-airship'; export function CustomPreferenceCenterScreen({ preferenceCenterId }) { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { loadConfig(); }, [preferenceCenterId]); async function loadConfig() { try { const cfg = await Airship.preferenceCenter.getConfig(preferenceCenterId); setConfig(cfg); } catch (error) { console.error('Failed to load config:', error); } finally { setLoading(false); } } async function toggleSubscription(listId: string, subscribe: boolean) { if (subscribe) { await Airship.contact.subscriptionLists.subscribe(listId); } else { await Airship.contact.subscriptionLists.unsubscribe(listId); } } if (loading) { return Loading...; } if (!config) { return Failed to load Preference Center; } return ( {config.display.name} {config.sections.map((section) => ( {section.display.name} {section.items.map((item) => ( {item.display.name} {item.display.description} toggleSubscription(item.subscriptionId, value) } /> ))} ))} ); } ``` > **Important:** Preference Center configuration is currently limited to subscription lists only. Use the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/react-native/audience/subscription-lists/) to manage user subscriptions. ### Audience Management Integrate audience management features into your React Native app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs and listen for channel creation. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. ```ts // Get the channel ID (may return null if not yet created) const channelId = await Airship.channel.getChannelId(); // Wait for the channel ID to be created (returns the channel ID once available) const channelId = await Airship.channel.waitForChannelId(); ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Use `waitForChannelId()` if you need to wait for the channel to be created before proceeding. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. ```ts Airship.addListener(EventType.ChannelCreated, (event) => { console.log('Channel created: ', event.channelId); }); ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring the takeOff options, see [React Native SDK Setup](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/getting-started/). ```ts await Airship.takeoff({ ... isChannelCaptureEnabled: false, }) ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during takeOff. For more information about Privacy Manager, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. ```typescript await Airship.contact.identify(namedUserId); ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. ```ts await Airship.contact.reset(); ``` You can get the Named User ID only if you set it through the SDK. ```ts const namedUser = await Airship.contact.getNamedUserId(); ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. ```ts Airship.channel.editTags() .addTags(["one", "two", "three"]) .removeTags(["some_tag"]) .apply() // Accessing channel tags const tags = await Airship.channel.getTags(); ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```ts Airship.channel.editTagGroups() .addTags("loyalty", ["silver-member"]) .removeTags("loyalty", ["bronze-member"]) .apply() ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```ts Airship.contact.editTagGroups() .addTags("loyalty", ["silver-member"]) .removeTags("loyalty", ["bronze-member"]) .apply() ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. ```ts Airship.channel.editAttributes() .setAttribute("device_name", "Bobby's Phone") .setAttribute("average_rating", 4.99) .removeAttribute("vip_status") .apply(); ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. ```ts Airship.contact.editAttributes() .setAttribute("first_name", "Bobby") .apply(); ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. ```ts Airship.contact.editAttributes() .setJsonAttribute("attribute_name", "instance_id", {"key":"value", "another_key":"another_value"}) .removeJsonAttribute("some_attribute_name", "some_instance_id") .apply() ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. ```ts // Modifying channel subscription lists Airship.channel.editSubscriptionLists() .subscribe("food") .unsubscribe("sports") .apply() // Fetching channel subscription lists const channelSubscriptions = await Airship.channel.getSubscriptionLists(); ``` ## Contact Subscription Lists Contact subscriptions are set at the user-level and require a Channel scope specifying the types that the subscription list applies to. ```ts // Modifying contact subscription lists Airship.contact.editSubscriptionLists() .subscribe("food", SubscriptionScope.App) .unsubscribe("sports", SubscriptionScope.Sms) .apply() // Fetching contact subscription lists const contactSubscriptions = await Airship.contact.getSubscriptionLists(); ``` ## Verifying Subscription Lists To verify that subscription lists have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ### Data Collection Overview of data collection and controls provided by the Airship React Native SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: | Privacy Manager Flag | Features | |----------------------|----------| | `push` | Push notifications | | `in_app_automation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | | `message_center` | Message Center | | `tags_and_attributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | | `contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | | `analytics` | Associated identifiers, Custom events, Screen tracking, Surveys, email address, Feature Flag interaction | | `feature_flags` | Feature Flag evaluation and interaction | | `all` | All features | | `none` | No features | ## Configuring default enabled features You can configure which features are enabled by default when the SDK initializes. This is done in your Airship config during `takeOff`. For information about setting up the Airship SDK and configuring the takeOff options, see [React Native SDK Setup](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/getting-started/). ### Default configuration (all features enabled) By default, all features are enabled. The SDK will collect data and make network requests as configured. ### Disabling all features To disable all features by default (useful for consent opt-in flows), set the enabled features to an empty array: ```typescript await Airship.takeOff({ default: { enabledFeatures: [] }, ... }); ``` ### Enabling specific features To enable only specific features by default: ```typescript await Airship.takeOff({ default: { enabledFeatures: ["push", "analytics"] }, ... }); ``` ## Modifying enabled features at runtime You can enable or disable features at any time after takeOff. Once you modify the enabled features from the default, those settings are persisted between app launches. ### Enabling features ```typescript await Airship.privacyManager.enableFeatures(["push", "analytics"]); ``` ### Disabling features ```typescript await Airship.privacyManager.disableFeatures(["push", "analytics"]); ``` ## Consent opt-in flow example A common use case is to start with all features disabled, then enable them as users grant consent: ```typescript // Start with all features disabled await Airship.takeOff({ default: { enabledFeatures: [] }, ... }); // Later, when user grants consent: async function userGrantedConsent() { // Enable features based on user's consent choices await Airship.privacyManager.enableFeatures(["push", "analytics"]); } ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Apple Privacy Manifest](https://www.airship.com/docs/reference/data-collection/apple-privacy-manifest/) - Declare data collection practices to Apple (iOS) - [Google Play Data Safety](https://www.airship.com/docs/reference/data-collection/google-play-data-safety/) - Reference for Google Play's Data Safety section (Android) - [Analytics](https://www.airship.com/docs/developer/sdk-integration/react-native/data-collection/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/react-native/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with custom events. They require enabling analytics for your app. For detailed information, see the [Custom Events guide](https://www.airship.com/docs/guides/audience/events/custom-events/). ```typescript // Import CustomEvent import Airship, { CustomEvent } from '@ua/react-native-airship'; // ... var customEvent: CustomEvent = { eventName: "event_name", eventValue: 123.12, properties: { "my_custom_property": "some custom value", "is_neat": true, "any_json": { "foo": "bar" } } } await Airship.analytics.addCustomEvent(customEvent); ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. ```typescript await Airship.analytics.associateIdentifier("key", "value"); ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. ```typescript await Airship.analytics.trackScreen("MainScreen"); ``` ### Troubleshooting Common issues and solutions for Airship module setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/getting-started/) or [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/advanced-configuration/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Installation Errors If you encounter errors during installation: - Verify that you're using a compatible version of React Native. See [Requirements](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/getting-started/#requirements) in *Getting Started*. - Ensure all native dependencies are properly linked. - Check that your iOS and Android projects are correctly configured. ## Initialization Errors If you encounter errors during SDK initialization: - Verify your credentials in the Airship dashboard. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. - Check that `takeOff` is called correctly in your app. - Ensure both iOS and Android native modules are properly installed. ## Expo: iOS Build Errors with Missing Headers If you encounter iOS build errors with messages like `'tuple' file not found`, `'iosfwd' file not found`, or `'generated/RNAirshipSpec/RNAirshipSpec.h' file not found`, this is typically caused by a compatibility issue between Expo and React Native's C++ headers. This usually happens when: - You're using `use_frameworks! :linkage => :static` in your Podfile - Your Expo SDK version has an incompatible `expo-dev-menu` dependency To fix this issue: - Add a resolution to your `package.json` to force a compatible version: ```json { "resolutions": { "expo-dev-menu": "X.X.X" // Replace with compatible version (e.g., 6.0.21) } } ``` - Or update `expo-dev-client` to the latest version. - Run `npm install` and `pod install` again. If you're running Firebase alongside Airship, see [Extending the FirebaseMessagingService](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#extending-the-firebasemessagingservice) for Android configuration. # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/react-native/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. ## Push Notifications Not Working If push notifications are not being received: - Verify that push notifications are enabled for both iOS and Android. - Check that APNs (iOS) and FCM (Android) are properly configured. - Ensure the app has notification permissions. ## Checking Push Notification Status If push notifications aren't working as expected, you can check the notification status to diagnose the issue: ```typescript const status = await Airship.push.getNotificationStatus(); console.log('User notifications enabled:', await Airship.push.isUserNotificationsEnabled()); console.log('Notifications allowed:', status.areNotificationsAllowed); console.log('Push token registered:', status.isPushTokenRegistered); console.log('Privacy feature enabled:', status.isPushPrivacyFeatureEnabled); console.log('User opted in:', status.isUserOptedIn); console.log('Fully opted in:', status.isOptedIn); ``` You can also listen for status changes: ```typescript Airship.addListener(EventType.PushNotificationStatusChangedStatus, (event) => { console.log('Notification status changed:', event.status); console.log('Is opted in:', event.status.isOptedIn); }); ``` ## Common Status Scenarios - `isUserOptedIn = false`: Check if `userNotificationsEnabled` is set to `true` and if the user granted permission. - `isPushPrivacyFeatureEnabled = false`: Push privacy feature is disabled in Privacy Manager. - `isPushTokenRegistered = false`: Device hasn't received a push token yet. Check network connectivity and platform configuration. - `isUserOptedIn = true` but `isOptedIn = false`: Push token registration is pending or failed. Check console logs for errors. ## Expo: Push Notifications Not Received If you don't receive Airship pushes in your Expo app, make sure you didn't previously install `expo-notifications` or another push provider by mistake. There can only be one service in each app that receives FCM messages, so it might create conflicts with Airship. If you do want to have another push provider alongside Airship, you will need to create your own `FirebaseMessagingService` to forward `onNewToken` and `onMessageReceived` calls to the Airship SDK. Follow the steps for [Extending the FirebaseMessagingService](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#extending-the-firebasemessagingservice) in the *Android SDK Setup* documentation. If you're running Firebase alongside Airship, see [Extending the FirebaseMessagingService](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#extending-the-firebasemessagingservice) for Android configuration. ## Unity Integrate the Airship SDK into your Unity applications for iOS and Android. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Message Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/unity/message-center/getting-started/) and [Preference Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/unity/preference-center/getting-started/). ```csharp UAirship.Shared.OnDeepLinkReceived += (string deepLink) => { // Handle deep link }; ``` # Unity Plugin Changelog > The latest updates to the Airship Unity plugin. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. [View Older Releases](https://github.com/urbanairship/ua-unity-plugin/releases?q=created%3A%3C2024-05-15&expanded=true) # Unity Plugin Resources > API documentation, source code, and changelogs for the Airship Unity plugin. ## API References * [API docs](https://www.airship.com/docs/reference/libraries/unity/latest/) ## GitHub * [Source](https://github.com/urbanairship/ua-unity-plugin) * [Example Behavior Script](https://github.com/urbanairship/ua-unity-plugin/blob/main/Assets/Scripts/UrbanAirshipBehaviour.cs) ## Changelog * [Unity Changelog](https://www.airship.com/docs/developer/sdk-integration/unity/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. ### SDK Installation Complete installation and configuration guides for the Airship Unity plugin. # Install and Set Up the Unity Plugin > How to install the Airship Unity plugin. ## Requirements * Unity 5+ * iOS: Xcode `15.3+` * iOS: Minimum deployment target iOS `15+` * Android: Android SDK installed and updated (requires `minSdkVersion` = `23+`) * Android: Using Android SDK manager, install API `36+`. * If a Custom Gradle Template is used, the gradle template needs to be configured to use API VERSION `36+`. ## Setup [Download](https://github.com/urbanairship/ua-unity-plugin/releases) the latest plugin and import the `unitypackage` into the Unity project: `Open Assets -> Import Package -> Custom Package`. ![Importing the Airship package in Unity](https://www.airship.com/docs/images/unity-assets-import-package_hu_a6880ed7081154dc.webp) *Importing the Airship package in Unity* Configure Airship Settings: `Open Window -> Airship -> Settings` and set the Airship settings. > **Important:** Leave the `Android FCM Sender ID` field **BLANK** for both Production and Development. > **Important:** If your app uses Airship's EU cloud site, you will need to configure that using the `Cloud Site` setting. ![Airship Settings window in Unity](https://www.airship.com/docs/images/unity-ua-settings_hu_94e022e909756bec.webp) *Airship Settings window in Unity* ![Airship configuration options in Unity](https://www.airship.com/docs/images/unity-ua-config_hu_1800978d81e08caf.webp) *Airship configuration options in Unity* If proguard is enabled, add Airship settings to the `proguard-user.txt` file: `-keep public class com.urbanairship.unityplugin.UnityPlugin -keepclassmembers class com.urbanairship.unityplugin.UnityPlugin { public ; public ; static ; }` For push notification setup, see the [Push Notifications Getting Started guide](https://www.airship.com/docs/developer/sdk-integration/unity/push-notifications/getting-started/). ## Troubleshooting A common error when setting up our Unity plugin is "Could not create an instance of type org.gradle.initialization.DefaultSettings_Decorated". If you encounter this error, take these steps in the Unity Editor: 1. Go to **Assets**, then **External Dependency Manager**, then **Android Resolver**, then **Force Resolve**. 1. Sometimes Unity has difficulty retrieving the JDK installed with your editor even when selected. To solve this: 1. Go to **Edit**, then **Preferences**, then **External Tools**. 1. For **JDK**, select **Copy Path**, uncheck **JDK Installed with Unity (Recommended)**, then paste the path. ![Setting the JDK path in the Unity Editor](https://www.airship.com/docs/images/unity/unity-uncheck-jdk-installed_hu_9c5a35b94f3157d2.webp) *Setting the JDK path in the Unity Editor* 1. ![Enabling project templates in the Unity Editor](https://www.airship.com/docs/images/unity/unity-enable-custom-gradle_hu_7a1ebe2c4fc15bd9.webp) *Enabling project templates in the Unity Editor* If the previous steps do not fix your issue: 1. Go to **Project Settings**, then **Player**, then **Android**, then **Publishing Settings**, then **Build**, and then select **Custom Main Gradle Template** and **Custom Gradle Properties Template**. 1. Go to **Assets**, then **External Dependency Manager**, then **Android Resolver**, then **Force Resolve**. If you don't see a channel ID or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/unity/troubleshooting/initialization/) for common problems and solutions. # Logging > Configure log levels to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. By default, the log level is set to **Info** for development builds and **Error** for production builds to ensure clean logs in a live environment. ## Log levels The following log levels are available, ordered from most to least verbose. | Log Level | Description | | :-------- | :---------- | | **Verbose** | Reports highly detailed SDK status, which is useful for deep debugging and troubleshooting. | | **Debug** | Reports general SDK status with more detailed information than `Info`. | | **Info** | Reports general SDK status and lifecycle events. | | **Warning** | Used for API deprecations, invalid setup, and other potentially problematic situations that are generally recoverable. | | **Error** | Used for critical errors, exceptions, and other situations that the SDK cannot gracefully handle. | | **None** | Disables all logging. | ## Configuring log levels Log levels can be set within the Airship config menu in Unity. # Locale > Configure locale behavior and override the default locale that Airship uses. > **Note:** Locale configuration is not supported in the Airship Unity plugin. ### Push Notifications Configure and implement push notifications for iOS and Android platforms. # Push Notifications > How to configure your application to receive and respond to notifications. Before you can send and receive push notifications, you need to configure your app for the platform(s) you're targeting. ## Platform Setup Follow the platform-specific setup instructions below to enable push notifications in your Unity app. ### iOS After generating a project for iOS, enable Push Notifications in the project editor's Capabilities pane: ![Enabling Push Notifications in the Xcode project editor](https://www.airship.com/docs/images/ios-enable-push-notifications_hu_d5790d72c4daf49a.webp) *Enabling Push Notifications in the Xcode project editor* #### Notification Service Extension

To take advantage of notification attachments, such as images, animated gifs, and video, you will need to create a notification service extension.

Follow the steps in the [iOS Notification Service Extension Guide](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/). ### Android Download the Android Firebase configuration file, `google-services.json`, from the application's Firebase console and add it to the `Assets` directory. If your Unity application does not have an associated application in the Firebase console, follow the [FCM setup instructions](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#fcm-setup) to set one up. ## Enable User Notifications Enabling `userNotificationsEnabled` will prompt the user for permission to send notifications. To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately, and instead wait for a more appropriate time in the app. The Airship SDK makes a distinction between `user notifications`, which can be seen by the user, and other forms of push that allow you to send data to your app silently, or in the background. Enabling or disabling user notifications is a preference often best left up to the user, so by default, user notifications are disabled. ```csharp UAirship.Shared.UserNotificationsEnabled = true; ``` > **Note:** For apps that target Android 13 (API 33) and above, enabling user notifications will display a runtime permission prompt to allow notifications to be sent. > > To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately on app startup, and instead wait for a more appropriate time to prompt for notification permission. ## Handle Notification Events The Airship SDK provides several callbacks for when a push is received or a notification is interacted with. Apps can use these callbacks to do custom push processing. Registering for a callback is optional, the SDK will automatically launch the application without the need to set a callback. ```csharp UAirship.Shared.OnPushReceived += (PushMessage message) => { // Handle message }; UAirship.Shared.OnPushOpened += (PushMessage message) => { // Handle message }; ``` ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. For Android, all push messages are delivered in the background, but default Airship will treat messages without an `alert` as silent. For iOS, set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). > **Note:** Pushes sent with the `content_available` property (iOS) or without an `alert` (Android) do not have guaranteed delivery. Factors affecting delivery include battery life, whether the device is connected to WiFi, and the number of silent pushes sent within a recent time period. These metrics are determined solely by iOS/Android and APNs/FCM. Therefore, this feature is best used for supplementing the regular behavior of the app rather than providing critical functionality. For instance, an app could use a silent push to pre-fetch new data ahead of time in order to reduce load times when the app is later launched by the user. ## iOS Notification Options > **Note:** iOS notification options, badges, and quiet time are not supported in Unity. If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/unity/troubleshooting/push-notifications/) to check notification status and fix common issues. ### Message Center Implement Message Center to provide an inbox for rich HTML-based messages. # Message Center > The default Message Center is available for Unity with minimal integration required. Basic theming options are supported. Message Center provides an inbox for rich, HTML-based messages that users can view at their convenience. By default, when your app receives a push notification with a Message Center action, the Message Center automatically displays. You can also display the Message Center manually by calling a simple method, making it easy to add a Message Center button to your app's navigation. Message Center inboxes are associated with channel IDs. Each device has a unique channel ID that persists across app launches, allowing users to access their message history. ## Display the Message Center Display the Message Center with a single method call: ```csharp UAirship.Shared.DisplayMessageCenter(); ``` ## Override Default Display Behavior Custom display behavior is not supported in Unity. The default Message Center will be displayed when requested. ## Fetch Messages Retrieve messages from the inbox: ```csharp UAirship.Shared.InboxMessages(); ``` ## Listen for Message Updates Subscribe to message updates using callbacks: ```csharp UAirship.Shared.InboxMessages(messages => { // Handle messages }); ``` ## Listen for Unread Count Changes Subscribe to unread count updates: ```csharp var unreadCount = UAirship.Shared.UnreadCount; // Update badge or UI ``` ## Refresh Messages Manually refresh the message list from the server: ```csharp UAirship.Shared.RefreshInbox(); ``` ## Mark Messages as Read Mark one or more messages as read: ```csharp UAirship.Shared.MarkInboxMessageRead("message-id"); ``` ## Delete Messages Delete one or more messages: ```csharp UAirship.Shared.DeleteInboxMessage("message-id"); ``` ### Preference Center Implement Preference Center to let users control their subscription preferences. # Preference Center > Preference Center allows users to opt in and out of subscription lists configured via the Airship Dashboard. > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. Preference Center provides a pre-built UI for users to manage their subscription preferences. Learn more in the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Display a Preference Center Display a Preference Center with a single method call: ```csharp UAirship.Shared.OpenPreferenceCenter("preference-center-id"); ``` ### Audience Management Integrate audience management features into your Unity app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs and listen for channel creation. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. ```csharp string channelId = UAirship.Shared.ChannelId; ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. ```csharp UAirship.Shared.OnChannelUpdated += (string channelId) => { Debug.Log ("Channel updated: " + channelId); }; ``` # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. ```csharp UAirship.Shared.NamedUserId = "some named user ID"; ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. ```csharp UAirship.Shared.NamedUserId = null; ``` You can get the Named User ID only if you set it through the SDK. ```csharp UAirship.Shared.NamedUserId ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. ```csharp // Add tag UAirship.Shared.AddTag ("some-tag"); // Remove tag UAirship.Shared.RemoveTag ("other-tag"); // Accessing channel tags IEnumerable tags = UAirship.Shared.Tags; ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```csharp UAirship.Shared.EditChannelTagGroups () .AddTag ("loyalty", "silver-member") .RemoveTag ("loyalty", "bronze-member") .Apply (); ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```csharp UAirship.Shared.EditNamedUserTagGroups () .AddTag ("loyalty", "silver-member") .RemoveTag ("loyalty", "bronze-member") .Apply (); ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. ```csharp UAirship.Shared.EditChannelAttributes() .SetAttribute("device_name", "Bobby's Phone") .SetAttribute("average_rating", 4.99) .RemoveAttribute("vip_status") .Apply() ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. ```csharp UAirship.Shared.EditNamedUserAttributes() .SetAttribute("first_name", "Bobby") .Apply() ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. ### Data Collection Overview of data collection and controls provided by the Airship Unity SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. > **Note:** Privacy Manager is not supported in the Airship Unity plugin. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with custom events. They require enabling analytics for your app. For detailed information, see the [Custom Events guide](https://www.airship.com/docs/guides/audience/events/custom-events/). ```csharp CustomEvent customEvent = new CustomEvent(); customEvent.EventName = "event_name"; customEvent.EventValue = 123.45; customEvent.AddProperty("my_custom_property", "some custom value"); customEvent.AddProperty("is_neat", true); UAirship.Shared.AddCustomEvent(customEvent); ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. ```csharp UAirship.Shared.AssociateIdentifier("key", "value"); ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. ```csharp UAirship.Shared.TrackScreen("MainScreen") ``` ### Troubleshooting Common issues and solutions for Airship plugin setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/unity/installation/getting-started/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Installation Errors If you encounter errors during installation: - Verify that you're using a compatible version of Unity. See [Requirements](https://www.airship.com/docs/developer/sdk-integration/unity/installation/getting-started/#requirements) in *Getting Started*. - Ensure the Unity package is properly imported. - Check that your iOS and Android build settings are correctly configured. ## Initialization Errors If you encounter errors during SDK initialization: - Verify your credentials in the Airship dashboard. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. - Check that `takeOff` is called correctly in your Unity scripts. - Ensure both iOS and Android native modules are properly configured. # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/unity/push-notifications/) aren't working as expected: - Verify that push notifications are enabled for both iOS and Android. - Check that APNs (iOS) and FCM (Android) are properly configured. - Ensure the app has notification permissions. ## Titanium Integrate the Airship SDK into your Titanium applications. # Titanium Module Changelog > The latest updates to the Airship Titanium module See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. [View Older Releases](https://github.com/urbanairship/titanium-module/releases?q=created%3A%3C2024-05-15&expanded=true) # Titanium Setup > How to install Airship Titanium module.## Resources * [Airship Titanium Module Release](https://github.com/urbanairship/titanium-module/releases) * [Titanium Example](https://github.com/urbanairship/titanium-module/blob/main/example/app.js) * [Github Repo](https://github.com/urbanairship/titanium-module) ## Requirements * Titanium 10.0.0+ * To use notifications: - [iOS APNs setup instructions](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration) - [Android FCM setup instructions](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#android-channel-configuration) ### iOS * Xcode `15.3+` * minimum deployment target iOS `15+` ### Android * minSdkVersion `23+` * compileSdkVersion `36+` ## Setup Start by [downloading](https://github.com/urbanairship/titanium-module/releases) the latest iOS and Android modules. Modify the `tiapp.xml` file to include and configure the Airship module. ### Configure Airship **Example app.js** ```js var Airship = require("ti.airship"); Airship.takeOff({ development: { appKey: "Your Development App Key", appSecret: "Your Development App Secret" }, production: { appKey: "Your Production App Key", appSecret: "Your Production App Secret" }, site: "us", // use "eu" for EU sites. urlAllowList: ["*"], // allows all URLs android: { notificationConfig: { icon: "ic_notification", accentColor: "#ff0000" } } }); ``` **tiapp.xml** ```xml ... ... ti.airship ti.airship ... UIBackgroundModes remote-notification ``` ### Android FCM Android requires the `google-services.json` file to be copied into the app's directory `platform/android/google-services.json`. ## Windows Integrate the Airship SDK into your Windows applications. (Deprecated) # Getting Started > The Airship Windows SDK supports Windows and Windows Phone devices, including Windows 10. > **Important:** Windows platform support is deprecated. Apps targeting Windows Phone 8.1 and higher should use the [Universal Library](#universal-library), which is compatible with Windows Desktop and Windows Phone via the WNS push service. Windows devices provide a unique notification user experience with **live tiles**, a set of app icon assets that can be customized to reflect your app's branding. Windows devices also support **toast notifications**, the familiar non-modal notification element often used to display a short, auto-expiring message in many Android and desktop applications. For details on the Windows notification user experience, see the following resources from Microsoft: * [UX guidelines for tiles and notifications](https://msdn.microsoft.com/en-us/library/windows/apps/dn611865.aspx) * [Windows Push Notification Services (WNS) overview](https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-windows-push-notification-services--wns--overview) ## Supported Features Airship provides Support for **Live Tiles** and **Toast** notifications. We support Tiles at the API Level only, and Toast both at the API level and in the dashboard. We support Toast Templates at the API level as well. See [Windows Push](https://www.airship.com/docs/developer/sdk-integration/windows/push-notifications/#windows-push) for examples of tiles and toast notifications. ## Key Terminology APID : Sometimes also described as a "Push ID", and APID is an app-level device identifier that can be used to target specific devices with push notifications. All Windows apps using the Airship library are assigned an APID, which takes the form of a UUID string, such as `e3d35643-26d3-4b0e-c632-959291744a02`. App Key and Secret : All applications registered with Airship have an associated key, secret and master secret, which are strings used in authentication with the Airship REST API. The app key is effectively an identifier. The app secret is used in authentication on the client side during registration. Master Secret : There is also another secret, known as the master secret, that is used in authentication for REST API actions such as sending push notifications. This is the only one of these that should be kept *truly secret*, because with this, anyone could could send push notifications on your behalf. > **Warning:** Always keep the master secret safe, and be sure never to expose it in your application code. To obtain these strings, create a new application on the Airship dashboard or log in to your existing app. In the sidebar on the left side of the screen, click *Details*. In the resulting window, you should find the key and secret at the top of the list. Here is an overview of the steps: ## Configure Services 1. Secure your [Windows Push Notification Services (WNS)](https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-windows-push-notification-services--wns--overview) service API keys from Microsoft. 1. For Windows 8+ and Windows Phone 8.1+ (WNS) see [How to authenticate with the Windows Push Notification Service (WNS) (Windows Runtime apps)](https://msdn.microsoft.com/en-us/library/windows/apps/hh465407.aspx). 1. Read our client documentation. (Below) 1. Integrate our client library into your development application, per the documentation. 1. Configure the Windows service in the [Dashboard](https://www.airship.com/docs/reference/glossary/#dashboard). 1. Deploy your application to the Windows Store. ## Universal Library This document provides reference information for implementing a client for a Windows desktop, phone or tablet (e.g. Surface tablet) device. ### Set Up Your Application in the Windows Store In order to be able to receive push notifications, you must set up your application in the Windows Store. If your app is not already registered with the Windows Store, please review [How to authenticate with the Windows Push Notification Service (WNS)](https://msdn.microsoft.com/en-us/library/windows/apps/hh465407) on MSDN, and follow the instructions before continuing. Once your app is registered with the Windows Store, and you have logged in as a developer, navigate to: *App »» Advanced Features »» Push notifications and Live Connect services info*. If you are submitting a Windows Phone 8.1 app, log in to the Windows Phone Dev Center, select your app, then navigate to the Details tab and look for the WNS heading. You will need to make note of the following pieces of information: * **CN and Identity Name**: Under the "Identifying your app" link on the sidebar. * **SID and Client Secret**: Under the "Authenticating your service" link on the sidebar The CN and Identity Name must now be added to your package manifest. You can set them manually on the "Packaging" tab, or you can let Visual Studio automatically associate your project with the application in the Windows Store by right clicking on the project and selecting Store->Associate App with the Store. ### Set Up Your Project with Airship It is customary on Airship to create two separate projects in the [Dashboard](https://www.airship.com/docs/reference/glossary/#dashboard), one for development and one for production. If you haven't already, perform this step now. In order to keep them distinct, it is recommended to choose names that reflect their purpose in the development lifecycle, e.g. "My Development App" vs. "My Production App". Repeat the process below for each app. > **Note:** You will need your Windows Push Notification Services (WNS): > > * Package security identifier (SID) > * Secret key > > You can find these in the [Partner Center](https://partner.microsoft.com/dashboard); follow the instructions on the App Management - WNS/MPNS page. 1. Next to your project name, select the dropdown menu (▼), then **Settings**. 1. Under **Channels**, select **Windows**. 1. Enter your SID and secret key. 1. Click **Save**. ## Client Library ### Add UrbanAirship.winmd To add the UrbanAirship runtime component to your application: 1. Unzip the distribution file (urban-airship-windows-.zip) 1. Copy the `UrbanAirshipLibrary` folder to a location in your project path. 1. Add a reference to `UrbanAirship.winmd`. ### Initialize the Airship Library The library must be initialized with your Airship application key and application secret. If you have created separate applications for development and production, as is recommended above, this is a good opportunity to specify the key/secret pair for each mode. ### Add `airshipconfig.xml` to your project **`airshipconfig.xml`** ```xml Development Key Development Secret Verbose false Production App Key Production App Secret Error ``` This file contains all the settings your app needs to register and receive push notifications. These settings can also be defined programmatically; see the integration section for details. ### Initialize the Airship Library **`App.xaml.cs`** ```c# using UrbanAirship; using UrbanAirship.Push; //... /// /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). /// public App() { this.InitializeComponent(); this.Suspending += OnSuspending; // Initialize and register Airship event listeners // See example implementations below this.registrationListener = new RegistrationEventListener(); this.pushReceivedListener = new PushReceivedEventListener(); this.pushActivatedListener = new PushActivatedEventListener(); } /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used when the application is launched to open a specific file, to display /// search results, and so forth. /// /// Details about the launch request and process. protected override void OnLaunched(LaunchActivatedEventArgs args) { Logger.Debug("Launched with: " + args.Arguments); Frame rootFrame = Window.Current.Content as Frame; // Do not repeat app initialization when the Window already has content, // just ensure that the window is active if (rootFrame == null) { // Create a Frame to act as the navigation context and navigate to the first page rootFrame = new Frame(); if (args.PreviousExecutionState == ApplicationExecutionState.Terminated) { //TODO: Load state from previously suspended application } // Place the frame in the current Window Window.Current.Content = rootFrame; } if (rootFrame.Content == null) { // When the navigation stack isn't restored navigate to the first page, // configuring the new page by passing required information as a navigation // parameter if (!rootFrame.Navigate(typeof(MainPage), args.Arguments)) { throw new Exception("Failed to create initial page"); } } // Ensure the current window is active Window.Current.Activate(); //Initialize and setup Airship library var config = AirshipConfig.ReadConfig("airshipconfig.xml"); UAirship.TakeOff(config); config.DebugLog(); PushManager.Shared.EnabledNotificationTypes = NotificationType.Toast; } ``` ### Configure Keys and Secrets in Code These configuration values can also be set programmatically: ```c# AirshipConfig config = new AirshipConfig(); config.DevelopmentAppKey = "DevelopmentAppKey"; config.DevelopmentAppSecret = "DevelopmentAppSecret"; config.ProductionAppKey = "ProductionAppKey"; config.productionAppSecret = "ProductionAppSecret"; #if DEBUG config.InProduction = false; #else config.InProduction = true; #endif UAirship.TakeOff(config); ``` ## Manifest Permissions To enable push notifications in your Windows Store app, you will need to add several items to your app's manifest (Package.appxmanifest). The items below are organized by tab **Application UI** On the "Application UI" tab, make sure that "Toast capable" is checked. **Capabilities** Make sure that "Internet" is checked. **Declarations** Push Channels have an expiration date and must be renewed periodically. To ensure that the channel is renewed even if the user hasn't launched the app recently, our library schedules a background task that will perform the renewal in the background. 1. Open the manifest (`Package.appxmanifest`). 1. On the *Declarations* tab, Add a *Background Task*. 1. Set the task type to *System event*. 1. Set the *Entry Point* to `UrbanAirship.Push.WNSChannelRenewalTask`. ![Background task declaration in the app manifest](https://www.airship.com/docs/images/w8_manifest_background_task_hu_d24a795919af8465.webp) *Background task declaration in the app manifest* # Push Notifications > Airship's SDK provides a simple interface for managing push notifications within your Windows app.> **Important:** Windows platform support is deprecated. ## Enabling and Disabling Notifications ```c# using UrbanAirship.Push; // Enable Toast and Tile notifications // Registers with WNS and UA // In Win8, if either type is enabled (toast or tile), the app will be able to receive _both_ types of notification. // Toast and Tile can be enabled separately in Windows Phone 8, so we have carried over that convention to W8 for // consistency. PushManager.Shared.EnabledNotificationTypes = NotificationType.Toast|NotificationType.Tile; // Disable notifications // Unregisters the device with WNS and UA PushManager.Shared.EnabledNotificationTypes = NotificationType.None; ``` Once the application is set up, notifications must be enabled. The code sample to the right shows how to enable and disable Toasts. ## Handling Push Events The Airship library communicates with your app through three main Event types: `RegistrationEvent`, `PushReceivedEvent` and `PushActivatedEvent`. `RegistrationEvent` : This event is fired once push registration is complete, both on the Microsoft side and with the Airship server infrastructure. A handler for this event will receive an instance of `RegistrationEventArgs`, which contains the APID identifier associated with your app, and a boolean indicating whether the APID is valid. If this boolean is true, then your app is fully registered and should now be able to receive push notifications. The library will normally attempt its own retries if registration fails, so in the unlikely case that the boolean is set to false, this indicates a fatal error was encountered (in this case, you should check the logs for a more complete report on the situation). `PushReceivedEvent` : This event is fired as soon as a push notification comes in to the app. The library will pass along the relevant notification data in an instance of `PushReceivedEventArgs`, at which point your application can decide whether to do any additional work with this information. `PushReceivedEventArgs` contains an instance of `UrbanAirship.Push.PushNotification`, which encapsulates the notification text as well as the raw content as received by the library. `PushActivatedEvent` : This event is fired as soon as a push notification is "activated" by the user, which is when the user clicks the notification while it is displayed on screen. This is a separate event from `PushReceivedEvent`, which will have already fired by the time this occurs. Handlers for this event will be passed an instance of `PushActivatedEventArgs`, which is effectively the same as `PushReceivedEventArgs`, containing an instance of the `UrbanAirship.Push.PushNotification` class for optional inspection and handling of the push payload. > **Note:** A Note on EventHandler Synchronization > > In the `UAirship.TakeOff` call, the Airship library captures the > current `SychronizationContext` and uses this to marshall the firing of > these events onto the UI thread. In the case where `TakeOff` is called on > a background thread, registration will continue as normal, but a warning > will be logged and all events will be fired on the current thread of > execution, which may result in your handlers being called on arbitrary > background threads. If this can't be avoided, it can still be mitigated > by explicitly synchronizing in your event handlers before touching UI > components or other thread-sensitive data, but for the most consistent > result we recommend ensuring that TakeOff is called on the UI thread. ### Example Event Listeners ```c# //example of a custom registration listener //this can be customized by the app developer private class RegistrationEventListener { //constructor, implicitly subscribes to the event public RegistrationEventListener() { PushManager.Shared.RegistrationEvent += RegistrationComplete; } //unsubscribes from the event public void Detach() { PushManager.Shared.RegistrationEvent -= RegistrationComplete; } //called when the event fires private void RegistrationComplete(object sender, RegistrationEventArgs e) { Logger.Info("Registration complete, apid: " + e.Apid + " valid: " + e.IsValid); } } //example of a custom push received listener //this can be customized by the app developer private class PushReceivedEventListener { //constructor, implicitly subscribes to the event public PushReceivedEventListener() { PushManager.Shared.PushReceivedEvent += PushReceived; } //unsubscribes from the event public void Detach() { PushManager.Shared.PushReceivedEvent -= PushReceived; } //called when the event fires private void PushReceived(object sender, PushReceivedEventArgs e) { Logger.Info("Push received!"); UrbanAirship.Push.PushNotification notification = e.Notification; if (notification.Type == UrbanAirship.Push.NotificationType.Toast) { ToastNotificationData data = notification.ToastData; Logger.Info("Text: "); foreach (var item in data.Text) { Logger.Info(item); } //if needed, the full XML payload is included for further inspection XmlDocument payload = (XmlDocument)data.Payload; Logger.Info("Full XML payload:"); Logger.Info(payload.GetXml()); } } } //example of a custom push activated listener //this can be customized by the app developer private class PushActivatedEventListener { //constructor, implicitly subscribes to the event public PushActivatedEventListener() { PushManager.Shared.PushActivatedEvent += PushActivated; } //unsubscribes from the event public void Detach() { PushManager.Shared.PushActivatedEvent -= PushActivated; } //called when the event fires private void PushActivated(object sender, PushActivatedEventArgs e) { Logger.Info("Push activated!"); UrbanAirship.Push.PushNotification notification = e.Notification; if (notification.Type == UrbanAirship.Push.NotificationType.Toast) { ToastNotificationData data = notification.ToastData; Logger.Info("Text: "); foreach (var item in data.Text) { Logger.Info(item); } //if needed, the full XML payload is included for further inspection XmlDocument payload = (XmlDocument)data.Payload; Logger.Info("Full XML payload:"); Logger.Info(payload.GetXml()); } } } // Add the listeners as private fields to be set in the constructor in the block above private RegistrationEventListener registrationListener; private PushReceivedEventListener pushReceivedListener; private PushActivatedEventListener pushActivatedListener; ``` ## Handling the Toast Launch String Toast notifications can be sent with a launch string. A launch string is an arbitrary value that will be passed to your application's `OnLaunched` handler. **`App.xaml.cs`** ```c# protected override void OnLaunched(LaunchActivatedEventArgs args) { Logger.Debug("Toast Launch String: " + args.Arguments); // Additional launch actions here } ``` ## Windows Push {#windows-push} WNS has several features not present in other platforms. These can be specified in the `wns` override in the `notification` object. ### Toast Notifications {#toast-notifications} Toast notifications are unique to Windows devices and can be specified as an attribute within the platform override for WNS notifications. In the example below, we specify the text in a toast alert in the `wns` object. A toast notification requires at least one visual element. For more information see [Toast schema](https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root). ```json { "audience": "all", "notification": { "wns": { "toast": { "binding": { "text": [ "Hello Airship!" ], "template": "ToastText01" } } } }, "device_types": ["wns"] } ``` #### Deep Link Use the `param` key to specify an optional URI parameter that specifies an XAML page to open in your app, along with any query string parameters. ```json { "audience": "all", "notification": { "wns": { "toast": { "text2": "This Just In!", "text1": "New developments", "param": "BreakingNewsPage.xaml?item=4" } } }, "device_types": ["wns"] } ``` #### Audio Optionally specify the toast alert sound with the `audio` key in the notification object. ```json { "audience": "all", "notification": { "wns": { "toast": { "binding": { "text": [ "Hello Airship!" ], "template": "ToastText01" }, "audio": { "sound": "reminder" } } } }, "device_types": ["wns"] } ``` #### Duration Optionally specify a display duration for your toast. There are two values: "short" (the default) and "long". Use "long" only if your notification is part of a scenario such as an incoming call or appointment reminder. For more information Windows devices have the ability to set the duration of a toast to be displayed on the device. ```json { "audience": "all", "notification": { "wns": { "toast": { "duration": "short" }, "binding": { "text": [ "Hello Airship!" ], "template": "ToastText01" } } }, "device_types": ["wns"] } ``` #### Templates {#toast-templates} Windows devices have the ability to specify a templated toast notification from Microsoft's list of [Toast Templates](https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Notifications.ToastTemplateType) document. ```json { "audience": "all", "notification": { "wns": { "toast": { "binding": { "text": [ "Hello Airship!" ], "template": "ToastText01" } } } }, "device_types": ["wns"] } ``` ### Badge Value Windows 8 devices have the ability to to specify a badge value to be used to set a number on the application tile, usually representing unread messages or waiting actions inside the application. ```json { "audience": "all", "notification": { "wns": { "badge": { "value": "1" } } }, "device_types": ["wns"] } ``` ### Badge Glyph In addition to numbers, there is also a fixed set of image glyphs that can be set on the badge. ```json { "audience": "all", "notification": { "wns": { "badge": { "glyph": "busy" } } }, "device_types": ["wns"] } ``` ### Tiles {#live-tiles} Windows Phones have the ability to update live tiles on the home screen. With the use of templates, you can update any tile on the home screen. ```json { "audience": "all", "notification": { "wns":{ "tile": { "count": 11, "wide_content_1": "Wide Content 1", "wide_content_2": "Wide Content 2", "wide_content_3": "Wide Content 3", "title": "Title", "template": "IconicTile", "background_color": "#FFAA0000" } } }, "device_types": ["wns"] } ``` ```json { "audience": "all", "notification": { "wns": { "tile": { "binding": [ { "text": [ "TileWideImageAndText01" ], "image": [ "/Assets/1.jpg" ], "template": "TileWideImageAndText01" } ] } } }, "device_types": ["wns"] } ``` # Segmentation > You can define your audience based on tags, alias, and APID.> **Important:** Windows platform support is deprecated. ## Setting Tags and an Alias ```c# using UrbanAirship.Push; // Create a list of tags (tags must be an IList) List tagList = new List(); tagList.Add("ATag"); tagList.Add("AnotherTag"); // Update the APID with the attributes set in the UI // Set the new values first here PushManager.Shared.Alias = "AnAliasString"; PushManager.Shared.Tags = this.Tags; // Now update them on the server // This does not need to be called if setting the tags or alias prior // to calling TakeOff() PushManager.Shared.UpdateRegistration(); ``` The APID is the primary push address for this application and device, but you may also register a set of tags or an alias for this APID. ## .NET Integrate the Airship SDK into your .NET MAUI applications for iOS and Android. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Message Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/dotnet/message-center/getting-started/) and [Preference Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/dotnet/preference-center/getting-started/). ```csharp Airship.Instance.OnDeepLinkReceived += (object sender, DeepLinkEventArgs e) => { Uri uri = new Uri(e.DeepLink); // Handle deep link }; ``` # .NET Package Changelog > The latest updates to the Airship .NET MAUI library. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 21.3.0 March 21, 2026 Minor release that updates the iOS SDK to 20.6.0 and Android SDK to 20.4.0, adding Native Message Center support, Scene improvements, and fixing deep links in Message Center messages on iOS. ### Changes - Updated iOS SDK to 20.6.0 - Updated Android SDK to 20.4.0 - Fixed deep links inside Message Center messages not working on iOS - Adjusted Markdown rendering in Scenes to be less aggressive when interpreting styling delimiters inside words - Improved Scene border rendering when rounded corners are present - Improved accessibility for single choice and multiple choice questions in Scenes (iOS) - Fixed Message Center unread indicator to only show for unread messages (iOS) ## 21.2.0 February 1, 2026 Minor release that adds programmatic deep link handling for iOS. ### Changes - Updated iOS SDK to 20.3.0 - Added `Airship.ProcessDeepLink()` for programmatic deep link handling (iOS) ## 21.1.0 January 30, 2026 ## 21.0.0 January 21, 2026 Major release that moves Message Center inbox functionality into the main package, updates native SDKs to 20.1.1, and upgrades to .NET 10. ### Changes - Updated to .NET 10.0 (`net10.0-android` and `net10.0-ios`) - Updated iOS SDK to 20.1.1 - Updated Android SDK to 20.1.1 - Moved Message Center inbox functionality (`IAirshipMessageCenter`, `Message` model) from `Airship.Net.MessageCenter` to `Airship.Net` - Changed Message Center access pattern from extension method to static property: `Airship.Instance.MessageCenter()` → `Airship.MessageCenter` - `Airship.Net.MessageCenter` package now contains only MAUI UI components (Controls) - Updated iOS minimum version to iOS 16+ - Updated MAUI Controls dependency to 9.0.0 See the [Migration Guide](https://github.com/urbanairship/airship-dotnet/blob/main/MIGRATION.md) for upgrade instructions. ## 20.2.1 January 2, 2026 Minor release to update SDKs and resolve crashes caused by calling iOS SDK methods on background threads instead of the main thread. ### Changes - Resolve crashes caused by calling iOS SDK methods on background threads instead of the main thread. - Updated iOS SDK to 19.11.5 - Updated Android SDK to 19.13.6 ## 20.2.0 October 8, 2025 Minor release that updates the SDKs and restores onMessageCenterDisplay. ### Changes - Restored onMessageCenterDisplay that was erroneously removed in version 20.0.0. - Updated iOS SDK to 19.11.0 - Updated Android SDK to 19.13.4 ## 20.1.0 August 26, 2025 Minor release that updates the SDKs and fixes a build signing issue with iOS. ### Changes - Removed embedded framework that failed to be signed during a release build. - Updated iOS SDK to 19.8.2 - Updated Android SDK to 19.11.0 ## 20.0.0 August 11, 2025 Major release with complete interface modernization and architectural improvements. ### Changes - Updated iOS SDK to 19.6.1 - Updated Android SDK to 19.8.0 - Complete interface modernization - split monolithic IAirship into module-specific interfaces - Changed access pattern from instance-based to static module properties - All async operations now return Tasks - Merged Message Center functionality into main Airship.Net package - Added iOS AirshipWrapper to handle Swift async method compatibility - Improved type safety and separation of concerns across modules ## 19.5.0 February 8, 2025 Minor release that updates the Android SDK to 18.7.0, including AndroidX library updates. ### Changes - Updated Android SDK to 18.7.0 ## 19.4.1 December 13, 2024 Minor release that updates the Airship.Net package to no longer depend on MAUI and adds methods to fetch channel and contact subscription lists to the cross-platform library. ### Changes - Removed unnecessary MAUI dependency from Airship.Net - Added `FetchChannelSubscriptionLists` and `FetchContactSubscriptionLists` methods to Airship.Net ## 19.4.0 July 29, 2024 Minor release that updates the Airship SDK to iOS 17.10.1 and Android 17.8.1. ### Changes - Updated iOS SDK to 17.10.1 - Updated Android SDK to 17.8.1 [View Older Releases](https://github.com/urbanairship/airship-dotnet/releases?q=created%3A%3C2024-05-15&expanded=true) # .NET Package Resources > API documentation, source code, and changelogs for the Airship .NET library. ## API References * [API docs](https://www.airship.com/docs/reference/libraries/maui/latest/) ## GitHub * [Source](https://github.com/urbanairship/airship-dotnet) * [Sample](https://github.com/urbanairship/airship-dotnet/tree/main/MauiSample) ## Changelog * [.NET Changelog](https://www.airship.com/docs/developer/sdk-integration/dotnet/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. ### SDK Installation Complete installation and configuration guides for the Airship .NET package. # Getting Started > How to install the Airship .NET package.We provide native binding packages for iOS and Android, and cross-platform .NET packages for core functionality and features: * **Airship Native Bindings**: The native bindings contain all the functionality of the iOS/Android SDKs, but provide no cross-platform interface. They can be found under the `UrbanAirship` namespace for Android and the `Airship` namespace for iOS. * **Airship .NET Library**: The `Airship.Net` package exposes a common subset of functionality between the iOS and Android SDKs. This library can be used within shared codebases (e.g., a .NET MAUI app). * **Airship .NET MessageCenter Library**: The `Airship.Net.MessageCenter` package exposes a custom message view control that can be used to display Message Center messages in shared codebases. ## Setup > **Note:** This guide applies to apps built with .NET MAUI. If your app uses Xamarin and Xamarin.Forms, please refer to the [Airship Xamarin Setup](https://www.airship.com/docs/developer/sdk-integration/dotnet/installation/getting-started/) guide. Before you begin, set up Push and any other Airship features for [Mobile](https://www.airship.com/docs/developer/sdk-integration/). The .NET bindings, like the SDKs that they wrap, are platform-specific, so this would be a good time to familiarize yourself with the SDK APIs and features for each platform you wish to target. ## Android Integration The following packages are available for Android integration with .NET MAUI. ### Android Packages | Package name | Description | |--------------------------------------|----------------------------------------------------------------| | Airship.Net.Android.Core | Core SDK support | | Airship.Net.Android.Adm | ADM push provider support | | Airship.Net.Android.Fcm | FCM push provider support | | Airship.Net.Android.Automation | In-App Automation, In-App Messaging, and Landing pages support | | Airship.Net.Android.MessageCenter | Message Center support | | Airship.Net.Android.PreferenceCenter | Preference Center support | | Airship.Net.Android.Layout | Layout support | | Airship.Net.Android.LiveUpdate | Live Update support | | Airship.Net.Android.FeatureFlag | Feature Flag support | ### Push Provider The Airship SDK for Android is split into modules which allow you to choose the push providers included in your application. You must install at least one push provider in your Android app. You can install more than one provider. Add the packages directly to your `.csproj` file by editing it and adding the appropriate `PackageReference` entries. Here's an example showing how to add the FCM push provider along with common feature modules: **Example `.csproj` with Android packages** ```xml ``` > **Note:** Replace `19.13.6` with the latest version available on [NuGet](https://www.nuget.org/packages?q=Airship.Net.Android). ### Airship Config Airship config options are a convenient way to pass custom settings to your app without needing to edit the source code. By default, the Airship SDK loads these settings from the `airshipconfig.properties` file located in your application's `Assets` directory for the Android platform. In the default MAUI single-project structure, this should be located at `Platforms/Android/Assets`. You may need to create the `Assets` directory, if it doesn't already exist. Use this file, among other things, to set the backend credentials for your app, and to toggle between development and production builds. In order for this file to be visible to the SDK during TakeOff, be sure that its `Build Action` is set to `AndroidAsset` in your app project. **Example `airshipconfig.properties`** ```ruby developmentAppKey = Your Development App Key developmentAppSecret = Your Development App Secret productionAppKey = Your Production App Key productionAppSecret = Your Production Secret # Toggles between the development and production app credentials # Before submitting your application to an app store set to true inProduction = false # LogLevel is "VERBOSE", "DEBUG", "INFO", "WARN", "ERROR" or "ASSERT" developmentLogLevel = DEBUG productionLogLevel = ERROR # Notification customization notificationIcon = ic_notification notificationAccentColor = #ff0000 ``` The `airshipconfig.properties` file should be placed in `Platforms/Android/Assets/` and its `Build Action` should be set to `AndroidAsset` in the properties panel. ### EU Cloud Site If your app uses Airship's EU cloud site, you will need to add that to `airshipconfig.properties`. ```ruby # EU Cloud Site site = EU ``` ### FCM-specific instructions Follow [FCM Android Setup](https://firebase.google.com/docs/android/setup) and [FCM Push Provider Setup](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#fcm-setup) to configure your application to use FCM. For MAUI apps, ensure your `google-services.json` file is placed in `Platforms/Android/` with `Build Action` set to `GoogleServicesJson`. ### TakeOff Create a class that extends `Autopilot` in `Platforms/Android` and register your Autopilot in the Android `AssemblyInfo.cs` file to generate the required metadata in the `AndroidManifest.xml` file. If needed, create a new `AssemblyInfo.cs` file in the `Platforms/Android/Properties/` directory. **Platforms/Android/SampleAutopilot.cs** ```c# using UrbanAirship; namespace ExampleApp; [Register("com.example.SampleAutopilot")] public class SampleAutopilot : Autopilot { public override void OnAirshipReady(UAirship airship) { // perform any post takeOff airship customizations } public override AirshipConfigOptions CreateAirshipConfigOptions(Context context) { /* Optionally set your config at runtime AirshipConfigOptions options = new AirshipConfigOptions.Builder() .SetInProduction(!BuildConfig.DEBUG) .SetDevelopmentAppKey("Your Development App Key") .SetDevelopmentAppSecret("Your Development App Secret") .SetProductionAppKey("Your Production App Key") .SetProductionAppSecret("Your Production App Secret") .SetNotificationAccentColor(ContextCompat.getColor(this, R.color.color_accent)) .SetNotificationIcon(R.drawable.ic_notification) .Build(); return options; */ return base.CreateAirshipConfigOptions(context); } } ``` **Platforms/Android/Properties/Assemblyinfo.cs** ```c# using Android.App; [assembly: MetaData("com.urbanairship.autopilot", Value = "com.example.SampleAutopilot")] ``` **Platforms/Android/MainActivity.cs** ```c# using UrbanAirship; namespace ExampleApp; [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] public class MainActivity : MauiAppCompatActivity { protected override void OnCreate (Bundle savedInstanceState) { Autopilot.AutomaticTakeOff(this.ApplicationContext); //... } } ``` ## iOS Integration The following packages are available for iOS integration with .NET MAUI. ### iOS Packages The Airship .NET component comes with full native bindings for the iOS SDK via the `Airship.Net.iOS.ObjectiveC` package. | Package name | Description | |-------------------------------|----------------------------------------------------| | Airship.Net.iOS.ObjectiveC | Full iOS SDK bindings (Core, Automation, Message Center, Preference Center, Feature Flags) | Add the iOS package directly to your `.csproj` file by editing it and adding the appropriate `PackageReference` entry: **Example `.csproj` with iOS package** ```xml ``` > **Note:** Replace `19.11.5` with the latest version available on [NuGet](https://www.nuget.org/packages/Airship.Net.iOS.ObjectiveC). ### Airship Config Provide an `AirshipConfig.plist` file with the application's configuration in the `Platforms/iOS` folder. In order for this file to be visible to the SDK during TakeOff, be sure that its `Build Action` is set to `BundleResource` in your app project. **Example `AirshipConfig.plist`** ```xml detectProvisioningMode developmentAppKey Your Development App Key developmentAppSecret Your Development App Secret productionAppKey Your Production App Key productionAppSecret Your Production App Secret ``` The `AirshipConfig.plist` file should be placed in `Platforms/iOS/` and its `Build Action` should be set to `BundleResource` in the properties panel. ### EU Cloud Site If your app uses Airship's EU cloud site, you will need to add that to `AirshipConfig.plist`. ```xml site EU ``` ### TakeOff The Airship SDK requires only a single entry point in the app delegate, known as *takeOff*. Inside your application delegate's `FinishedLaunching` method, initialize a shared `UAirship` instance by calling `takeOff`. This will bootstrap the SDK and look for settings specified in the `AirshipConfig.plist` config file. > **Note:** If the `takeOff` process fails due to improper or missing configuration, the shared > `UAirship` instance will be `null`. The Airship SDK always logs implementation errors at > high visibility. **Example takeOff** ```c# using Airship; namespace ExampleApp; [Register ("AppDelegate")] public class AppDelegate : MauiUIApplicationDelegate { protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions) { // Populate AirshipConfig.plist with your app's info from https://go.urbanairship.com // or set runtime properties here. NSError configError; UAConfig config = UAConfig.DefaultConfigWithError(out configError); if (config == null || configError != null) { throw new InvalidOperationException($"Failed to load Airship configuration: {configError?.LocalizedDescription ?? "Unknown error"}"); } NSError error; bool success = UAirship.TakeOff(config, launchOptions as NSDictionary, out error); if (!success || error != null) { throw new InvalidOperationException($"Failed to initialize Airship: {error?.LocalizedDescription ?? "Unknown error"}"); } // Configure Airship here return base.FinishedLaunching(application, launchOptions); } } ``` ### Notification Service Extension In order to take advantage of iOS notification attachments, such as images, animated gifs, and video, you will need to create a Notification Service Extension target in your project. Follow the [iOS Notification Service Extension Guide](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/) for setup instructions. ### Notification Content Extension The iOS SDK's Notification Content Extension provides support for carousel UI. Follow the [iOS Notification Content Extension Guide](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#notification-content-extension) for setup instructions. ## .NET Shared Components Installation {#maui-component-installation} Installing the Airship components is a quick and easy process, seamlessly integrated into Visual Studio. You have two installation options: * **Native bindings**: If you are only working with one platform, or if there is no reason for you to have a shared codebase between your platform projects, this may be an appropriate option. It's possible to make use of the native bindings * **Airship.NET + native bindings**: If you are working with multiple platforms, and you have a shared codebase (e.g., a MAUI app), this may be an appropriate option. You can use the Airship .NET libraries in the shared codebase, while the native bindings can handle platform-specific calls in each platform folder or project. Platform-specific native bindings can also be called directly from cross-platform code, using [conditional compilation](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/invoke-platform-code?view=net-maui-8.0#conditional-compilation). All components can be installed by editing your `.csproj` file directly or via the NuGet package manager in Visual Studio or the `dotnet` CLI. > **Note:** After adding platform-specific binding packages, inspect your `.csproj` file to make sure that > the `ItemGroup` or `PackageReference` includes the correct `Condition` attribute for your .NET > version and target platform: > > ```xml > > > > > > > > > > > > ``` ### Cross-Platform Library To use the cross-platform Airship .NET library, add it to your `.csproj` file: ```xml ``` > **Note:** The Airship SDK packages were updated in 2018 to reflect the modularization of the Android SDK, which is > now split into Core, ADM, FCM, and a set of feature modules. The original package named `urbanairship` is deprecated > and out of date, but currently remains in NuGet. The `airship.netstandard` package is not compatible with .NET MAUI > and is only appropriate for use in apps built with the older Xamarin Forms framework. > > When in doubt, check the release date of the package before installing. ## Native Bindings Using the native binding libraries is similar to using either the Android or iOS SDKs. Below we provide a simple comparison between setting a named user ID in the native SDK and binding library. In general, the two changes you will notice between the bindings and SDKs are: * Method calls are generally capitalized. * Getters/setters are generally converted into properties. ### Android **Native Java Call** ```java // Set the named user ID UAirship.shared().contact().identify("NamedUserID"); ``` **Binding Library** ```c# // Set the named user ID UAirship.Shared().Contact.Identify("NamedUserID"); ``` For more information on the Android SDK, please see the [Android platform documentation](https://www.airship.com/docs/developer/sdk-integration/android/). ### iOS **Native Swift Call** ```swift // Set the named user ID Airship.contact.identify("NamedUserID") ``` **Binding Library** ```c# // Set the named user ID UAirship.Contact.Identify("NamedUserID"); ``` For more information on the iOS SDK, please see the [iOS platform documentation](https://www.airship.com/docs/developer/sdk-integration/apple/). ## Airship .NET Library The Airship .NET library provides a unified interface for common SDK calls, allowing users to place common code in a shared location. This is ideal for working with MAUI -- simply add the `Airship.Net` NuGet dependency and all of these calls should be available from your shared codebase. > **Note:** Because the Airship.NET library currently has no shared interface for initializing > the app (i.e., calling `takeOff`), you must install the native bindings for each platform target and add platform-specific calls to initialize Airship > in your platform-specific sources or projects. The Airship .NET library is accessible through the `Airship` class, found in the `AirshipDotNet` namespace. Full documentation for the .NET library can be found [here](https://www.airship.com/docs/reference/libraries/maui/latest/). If you don't see a channel ID in the logcat or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/dotnet/troubleshooting/initialization/) for common problems and solutions. # Logging > Configure log levels to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. By default, the log level is set to **Info** for development builds and **Error** for production builds to ensure clean logs in a live environment. ## Log levels The following log levels are available, ordered from most to least verbose. | Log Level | Description | | :-------- | :---------- | | **Verbose** | Reports highly detailed SDK status, which is useful for deep debugging and troubleshooting. | | **Debug** | Reports general SDK status with more detailed information than `Info`. | | **Info** | Reports general SDK status and lifecycle events. | | **Warning** | Used for API deprecations, invalid setup, and other potentially problematic situations that are generally recoverable. | | **Error** | Used for critical errors, exceptions, and other situations that the SDK cannot gracefully handle. | | **Assert** | Disables all logging. | ## Configuring log levels You can set the log level in the Airship config files for Android and iOS. ### Android In your `airshipconfig.properties` file: ```text # Available log levels: VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT productionLogLevel = VERBOSE developmentLogLevel = VERBOSE ``` ### iOS In your `AirshipConfig.plist` file: ```xml productionLogLevel TRACE developmentLogLevel TRACE ``` # Locale > Configure locale behavior and override the default locale that Airship uses. > **Note:** Locale configuration is not supported in the Airship .NET library. Use native binding methods instead. ### Push Notifications Configure and implement push notifications for iOS and Android platforms. # Push Notifications > How to configure your application to receive and respond to notifications. ## Enable User Notifications Enabling `userNotificationsEnabled` will prompt the user for permission to send notifications. To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately, and instead wait for a more appropriate time in the app. The Airship SDK makes a distinction between `user notifications`, which can be seen by the user, and other forms of push that allow you to send data to your app silently, or in the background. Enabling or disabling user notifications is a preference often best left up to the user, so by default, user notifications are disabled. ```csharp Airship.Instance.UserNotificationsEnabled = true ``` > **Note:** For apps that target Android 13 (API 33) and above, enabling user notifications will display a runtime permission prompt to allow notifications to be sent. > > To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately on app startup, and instead wait for a more appropriate time to prompt for notification permission. ## Handle Notification Events The Airship SDK provides several callbacks for when a push is received or a notification is interacted with. Apps can use these callbacks to do custom push processing. Registering for a callback is optional, the SDK will automatically launch the application without the need to set a callback. > **Note:** Notification callbacks are not supported in Airship.NET library. Use native binding methods instead. ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. For Android, all push messages are delivered in the background, but default Airship will treat messages without an `alert` as silent. For iOS, set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). > **Note:** Pushes sent with the `content_available` property (iOS) or without an `alert` (Android) do not have guaranteed delivery. Factors affecting delivery include battery life, whether the device is connected to WiFi, and the number of silent pushes sent within a recent time period. These metrics are determined solely by iOS/Android and APNs/FCM. Therefore, this feature is best used for supplementing the regular behavior of the app rather than providing critical functionality. For instance, an app could use a silent push to pre-fetch new data ahead of time in order to reduce load times when the app is later launched by the user. ## iOS Notification Options > **Note:** iOS notification options, badges, and quiet time are not supported in Airship.NET library. Use native binding methods instead. If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/dotnet/troubleshooting/push-notifications/) to check notification status and fix common issues. ### Message Center Implement Message Center to provide an inbox for rich HTML-based messages. # Message Center > The default Message Center is available for .NET MAUI with minimal integration required. Basic theming options are supported. Message Center provides an inbox for rich, HTML-based messages that users can view at their convenience. By default, when your app receives a push notification with a Message Center action, the Message Center automatically displays. You can also display the Message Center manually by calling a simple method, making it easy to add a Message Center button to your app's navigation. Message Center inboxes are associated with channel IDs. Each device has a unique channel ID that persists across app launches, allowing users to access their message history. ## Display the Message Center Display the Message Center with a single method call: ```csharp Airship.Instance.DisplayMessageCenter(); ``` ## Override Default Display Behavior To use a custom Message Center implementation instead of the default UI, set an event handler: ```csharp Airship.Instance.OnMessageCenterDisplay += OnMessageCenterDisplay; static void OnMessageCenterDisplay(object sender, MessageCenterEventArgs e) { // Navigate to your custom Message Center UI // e.MessageId is optional - null means show the full message list } ``` ## Fetch Messages Retrieve messages from the inbox: ```csharp var messages = Airship.Instance.InboxMessages; ``` ## Listen for Message Updates Subscribe to message updates using events: ```csharp Airship.Instance.InboxMessages(messages => { // Handle messages }); ``` ## Listen for Unread Count Changes Subscribe to unread count updates: ```csharp var unreadCount = Airship.Instance.UnreadCount; // Update badge or UI ``` ## Refresh Messages Manually refresh the message list from the server: ```csharp Airship.Instance.FetchInboxMessages(success => { // Handle result }); ``` ## Mark Messages as Read Mark one or more messages as read: ```csharp Airship.Instance.MarkMessageRead("message-id"); ``` ## Delete Messages Delete one or more messages: ```csharp Airship.Instance.DeleteMessage("message-id"); ``` # Advanced Customizations > Airship's SDK provides a simple interface for managing the Message Center within your .NET MAUI application. This guide covers creating custom Message Center implementations for .NET MAUI applications. ## Prerequisites Message Center requires the Airship .NET MAUI SDK to be installed. See the [SDK installation guide](https://www.airship.com/docs/developer/sdk-integration/dotnet/installation/getting-started/) for setup instructions. ## Custom Message Center Implementation For complete control over Message Center placement and navigation, create a custom implementation using the Message Center components. ### Custom Display Handling Set up custom display handling: ```csharp Airship.Instance.OnMessageCenterDisplay += OnMessageCenterDisplay; static void OnMessageCenterDisplay(object sender, MessageCenterEventArgs e) { // Navigate to your custom Message Center UI // e.MessageId is optional - null means show the full message list } ``` ## Related Documentation - [Getting Started with Message Center](https://www.airship.com/docs/developer/sdk-integration/dotnet/message-center/getting-started/) - [Installing the Airship SDK](https://www.airship.com/docs/developer/sdk-integration/dotnet/installation/getting-started/) ### Preference Center Implement Preference Center to let users control their subscription preferences. # Preference Center > Preference Center allows users to opt in and out of subscription lists configured via the Airship Dashboard. > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. Preference Center provides a pre-built UI for users to manage their subscription preferences. Learn more in the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). > **Note:** Preference Center display is not supported in the Airship.NET library. Use native platform methods or build a custom implementation. # Advanced Customizations > Advanced customization options for Preference Center. This guide covers creating custom Preference Center implementations for .NET MAUI applications. ## Prerequisites Preference Center requires the Airship .NET MAUI SDK to be installed. See the [SDK installation guide](https://www.airship.com/docs/developer/sdk-integration/dotnet/installation/getting-started/) for setup instructions. ## Custom Preference Centers Preference Center is not supported in the Airship.NET library. Use native binding methods instead. ## Related Documentation - [Getting Started with Preference Center](https://www.airship.com/docs/developer/sdk-integration/dotnet/preference-center/getting-started/) - [Installing the Airship SDK](https://www.airship.com/docs/developer/sdk-integration/dotnet/installation/getting-started/) ### Audience Management Integrate audience management features into your .NET app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs and listen for channel creation. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. ```csharp string channelId = Airship.Instance.ChannelId; ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. ```csharp static void OnChannelCreation(object sender, ChannelEventArgs args) { Console.WriteLine("Channel created: " + args.ChannelId); } Airship.Instance.OnChannelCreation += OnChannelCreation; ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during initialization. For more information about Privacy Manager, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. ```csharp Airship.Instance.Contact.Identify("some named user ID"); ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. ```csharp Airship.Instance.Contact.Reset(); ``` You can get the Named User ID only if you set it through the SDK. ```csharp string namedUserId = await Airship.Instance.Contact.GetNamedUserIdAsync(); ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. ```csharp // Add tags Airship.Instance.Channel.EditTags() .Add("one") .Add("two") .Add("three") .Apply(); // Add a tag Airship.Instance.Channel.EditTags() .Add("a_tag") .Apply(); // Remove a tag Airship.Instance.Channel.EditTags() .Remove("a_tag") .Apply(); // Accessing channel tags IEnumerable tags = await Airship.Instance.Channel.GetTagsAsync(); ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```csharp Airship.Instance.Channel.EditTagGroups() .Add("silver-member", "loyalty") .Add("gold-member", "loyalty") .Set(new string[] { "bingo" }, "games") .Remove("bronze-member", "loyalty") .Remove("club-member", "loyalty") .Apply(); ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```csharp Airship.Instance.Contact.EditTagGroups() .Add("silver-member", "loyalty") .Add("gold-member", "loyalty") .Set(new string[] { "bingo" }, "games") .Remove("bronze-member", "loyalty") .Remove("club-member", "loyalty") .Apply(); ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. ```csharp Airship.Instance.Channel.EditAttributes() .SetAttribute("device_name", "Bobby's Phone") .SetAttribute("average_rating", 4.99) .RemoveAttribute("vip_status") .Apply(); ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. ```csharp Airship.Instance.Contact.EditAttributes() .SetAttribute("first_name", "Bobby") .Apply(); ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. ```csharp // Modifying channel subscription lists Airship.Instance.Channel.EditSubscriptionLists() .Subscribe("food") .Unsubscribe("sports") .Apply(); // Fetching channel subscription lists List channelSubscriptions = await Airship.Instance.Channel.FetchSubscriptionLists(); ``` ## Contact Subscription Lists Contact subscriptions are set at the user-level and require a Channel scope specifying the types that the subscription list applies to. ```csharp // Modifying contact subscription lists Airship.Instance.Contact.EditSubscriptionLists() .Subscribe("food", ChannelScope.App) .Unsubscribe("sports", ChannelScope.Sms) .Apply(); // Fetching contact subscription lists Dictionary> subscriptions = await Airship.Instance.Contact.GetSubscriptionListsAsync(); ``` ## Verifying Subscription Lists To verify that subscription lists have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ### Data Collection Overview of data collection and controls provided by the Airship .NET SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. > **Note:** Privacy Manager is not supported in the Airship .NET library. Use native binding methods instead. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with custom events. They require enabling analytics for your app. For detailed information, see the [Custom Events guide](https://www.airship.com/docs/guides/audience/events/custom-events/). ```csharp CustomEvent customEvent = new CustomEvent(); customEvent.EventName = "event_name"; customEvent.EventValue = 123.45; customEvent.AddProperty("my_custom_property", "some custom value"); customEvent.AddProperty("is_neat", true); Airship.Instance.AddCustomEvent(customEvent); ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. ```csharp Airship.Instance.AssociateIdentifier("key", "value"); ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. ```csharp Airship.Instance.TrackScreen("MainScreen"); ``` ### Troubleshooting Common issues and solutions for Airship library setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/dotnet/installation/getting-started/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Installation Errors If you encounter errors during installation: - Verify that you're using a compatible version of .NET MAUI. - Ensure the NuGet package is properly installed. - Check that your iOS and Android projects are correctly configured. ## Initialization Errors If you encounter errors during SDK initialization: - Verify your credentials in the Airship dashboard. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. - Check that `takeOff` is called correctly in your app. - Ensure both iOS and Android native modules are properly configured. # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/dotnet/push-notifications/) aren't working as expected: - Verify that push notifications are enabled for both iOS and Android. - Check that APNs (iOS) and FCM (Android) are properly configured. - Ensure the app has notification permissions. ## Web Integrate the Airship SDK into your website or web application. # Getting Started > This is a developer's guide for setting up Airship web push notifications and Scenes.Set up your project to register users for [Web Push Notifications](https://www.airship.com/docs/reference/glossary/#web_push_notification) and Web [Scenes](https://www.airship.com/docs/reference/glossary/#scene). Both are referred to generally as "notifications" and "web notifications" on this page. Airship supports web push notifications and Scenes on:
  • Desktop: Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, and Safari (v16 and above)
  • Android Mobile: Google Chrome, Mozilla Firefox, Opera
  • iOS and iPadOS Mobile: Safari (iOS and iPadOS 16.4 and above, as a standalone web app)
The Airship Web SDK is hosted on a secure content delivery network (CDN). In order to enable these notifications, you will add two components to your site: 1. An asynchronous loading snippet that allows use of the SDK before and after it is fully loaded. 1. A service worker that handles incoming push requests and communicates with the Airship Service. > **Tip:** If you use an AI coding assistant, you can connect it to Airship with Skills and an MCP server. See [Airship AI Tools](https://www.airship.com/docs/developer/ai-tools/ai-tools/). ## Resources * [Web SDK Reference](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/index.html) * [Web Platform Overrides](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#weboverrideobject) ## Airship Setup > **Important:** Airship Web SDK v2 was released August 1, 2024. > > * **Channel configuration** — The default title, action URL, and icon URL for web push are required for configuring the Web channel and will be included in the SDK bundle's JavaScript snippet, even if you intend to only use Web [Scenes](https://www.airship.com/docs/reference/glossary/#scene) in your project. > > * **Opt-in** — User opt in is not required for Web Scenes. You only need an opt-in prompt for web push. > > * **Billing** — If you register channels for Web users who do not opt in to web notifications, you are agreeing to be billed for [Monthly Unique Visitors](https://www.airship.com/docs/reference/billing/#monthly-unique-visitors). This may require an amendment to existing contracts. Contact your account manager for details. > > Removed support for Web SDK v2: > > * **AMP** — We will continue to support Web configurations that previously had Accelerated Mobile Pages (AMP) enabled, but AMP is not supported for new configurations. > > * **Secure bridge** — For Web SDK v1, we provided a workaround script for sites that were not fully HTTPS. This workaround was not required for non-HTTPS sites on Safari. Secure bridge is not supported for v2 integrations. > > We will continue to support Web configurations that previously had Secure Bridge enabled, but we no longer provide the script or setup instructions for sites still on Web SDK v1. > > * **Safari versions older than 16** — Web SDK v1 supports Safari versions older than 16, which requires an Apple Safari Web Push certificate in your Airship Web channel configuration. This is not supported for Web SDK v2 integrations. > > If your Web channel was already configured for Safari support, you must either: > > * **Maintain your existing Safari certificate** — This option is for supporting existing users who have opted in to web notifications, even if/when they upgrade to Safari 16 or above. See [Apple Safari Web Push certificates](#apple-safari-web-push-certificates) below. > * **Rebuild your Safari audience** — If you stop maintaining your Safari certificate, users will re-register upon their next visit to your website since they are already opted in at the browser level. > > Upgrading to Web SDK v2: > > * For Web channels already configured for AMP, Secure Bridge, and/or Safari versions older than 16, you will see an **Upgrade** button in the Web channel settings. > * After selecting **Upgrade** and confirming understanding of the consequences: > 1. The options for AMP, Secure Bridge, and Safari configuration will no longer appear in the Web channel settings. > 1. You must update your website. See [Updating an Existing Integration](https://www.airship.com/docs/developer/sdk-integration/web/v2-sdk/#updating-an-existing-integration) in *Implementing Web SDK v2*. Begin by configuring your site in the dashboard. Navigation to the settings differs depending on whether you have App or Web [Scenes](https://www.airship.com/docs/reference/glossary/#scene) enabled for your project. > **Warning:** The downloadable SDK bundle is for Web SDK v2 only. If you need to update your web push default values (Title, Action, Icon URL) and **do not need to migrate to v2**, do not make changes to your Web channel configuration. Instead, directly edit the content from [your `snippet.html`](#add-javascript-snippet-to-web-pages) that you added to each page of your website. 1. Next to your project name, select the dropdown menu (▼), then **Settings**. 1. Under **Channels**, select **Web**. 1. Configure default values for each field. The preview updates as you enter information. You can override your default values on a per message basis via the dashboard or API. | Field | Description | Steps | | --- | --- | --- | | **Default Title** | The bolded title that appears above the message body, typically your brand name | Enter text. | | **Default Action URL** | The URL that opens when a user clicks/taps your message, typically your brand's homepage URL | Enter a publicly accessible URL. | | **Default Icon URL** | The path to the image that will appear in your web push notifications. If you have CDN enabled, you also have the option to upload an icon. See also [Web icon](https://www.airship.com/docs/reference/messages/media-guidelines/#web-icon) in our _Media guidelines_ documentation. | Enter a publicly accessible URL or select **Upload icon** and select an image file. | 1. Manage the following options for websites where they are already in use. The options are not present when configuring a Web channel for the first time. | Option | Description | Steps | | --- | --- | --- | | **Secure Bridge** | Allows support for non-secure (non-HTTPS) websites | Toggle to enable or disable. | | **AMP Support** | Allows support for notifications on [Accelerated Mobile Pages](https://en.wikipedia.org/wiki/Accelerated_Mobile_Pages) | Toggle to enable or disable. | | **Safari Configuration** | For configurations supporting Safari versions older than 16, these are the domains that are allowed to request Safari registration permission from the user, and the required [Apple Safari Web Push certificate](#apple-safari-web-push-certificates) .p12 file and password | Enter the domains, starting with "http://" or "https://", either comma-separated or one per line. To replace the certificate, select **Choose File**, upload your .p12 file, and then enter the certificate password, if any. | 1. Select **Save**. The button will be labeled **Update** if you are editing an existing configuration. 1. Select **Download SDK Bundle** and save the zip file. 1. Unzip the bundle so you can use its files to complete the next steps. > **Important:** When you edit your web channel configuration, you must ALSO update your website with the new configuration files. > > After making changes and selecting **Update**, complete the final steps in the above procedure: download and unzip your new SDK bundle. Then you can proceed with the web SDK steps. ## Add JavaScript Snippet to Web Pages Locate file `snippet.html` in your unzipped SDK bundle and add the file content to all pages of your site. This file provides an asynchronous loading snippet that allows use of the Web SDK before it loads and comes populated with the configuration values required for your site. ### UA Object The loading snippet adds a `UA` object to the global scope. This object is a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which resolves to the SDK object: ```js const sdk = await UA const {channelId} = await sdk.create() console.log("Channel ID: %s", channelId) ``` > **Note:** Our documentation uses the async/await syntax for promises, which may not be available to you depending on your environment. If this is the case, you may use the classic `then` syntax, such as: > > ```js > UA.then(function (sdk) { > sdk.create().then(function (result) { > console.log("Channel ID: %s", result.channelId) > }) > }) > ``` ### iOS and iPadOS Safari Apple added Safari support for web push in iOS and iPadOS 16.4. To use web notifications and Scenes on iOS/iPadOS devices, Apple first requires a user to save the website to their home screen as a web app. This essentially means bookmarking a web page by selecting the *Share* action and then *Add to Home Screen*. After that, you can direct users to take specific actions to opt in to receiving notifications. Once opted in, users can manage those permissions per web app/site in their device *Notifications* settings. The notifications from web apps work exactly like notifications from native iOS and iPadOS apps and appear on the Lock Screen, in Notification Center, and on a paired Apple Watch. > **Important:** This is not a comprehensive guide on Home Screen web pages. If you wish to add full support for these features, it's recommended you see the following articles for further details: > > * [WebKit: Release Announcement](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/) > * [Apple: Supported Meta Tags](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html) In preparation for sending web notifications to your iOS and iPadOS users, you will need to configure your site to operate as a web app. At a minimum, add the following metadata to the `head` of your pages, which specifies to the browser that an app can run in "standalone" mode: **Site Metadata** ```html ``` > **Note:** The Airship-provided [HTML Prompt plugin](https://www.airship.com/docs/developer/sdk-integration/web/plugins/html-prompt/) does not prompt a browser that is in an unsupported context. It does not instruct the user to take any further action. If you are handling your own opt-in, you may also wish to handle iOS/iPadOS Safari opt-in differently, as calling `sdk.register()` will fail if the app is not running in standalone mode. The following [SDK Properties](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/UaSDK.html) are available: - `sdk.isSupported` is `true` if the SDK can load and track events; web push support is not considered - `sdk.isWebPushSupported` is `true` if the browser is supported _and_ supports web push; it does not consider if the current _context_ is supported (such as requiring it be added to the home screen on iOS) - `sdk.isAllowedPushRegistrationContext` is `true` if the browser is currently in an allowed registration context; note this does not otherwise check for browser features, so should only be checked after an `sdk.isWebPushSupported` check - `sdk.canRegister()` is a comprehensive check against _all_ checks required for web push registration, and returns a promise which resolves to `true` if: - the browser supports web push - the page is a secure context - the browser is in an allowable context (such as being saved to the home screen on iOS) - push permission has not already been denied When registering for web push, if `isSupported` is `true` and the value returned from `canRegister()` resolves to `false`, your site may be in a context that disallows registration. You can prompt the user to add the page to their home screen if they wish to receive notifications. This can also be detected using the [`Navigator.standalone`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator#non-standard_properties) property, which is only available on iOS/iPadOS Safari, using code similar to the following: **iOS Safari Context Detection** ```javascript const sdk = await UA const canRegister = await sdk.canRegister() if (sdk.isSupported && !canRegister) { if ('standalone' in navigator) { // this is an iOS Safari context; prompt the user } } ``` ## Place Service Worker Web push employs a *service worker*, which runs as a background process until woken up to perform SDK tasks, e.g., displaying notifications. Locate the service worker file `push-worker.js` in your unzipped SDK bundle and place it in the **root directory of your site**. It is imperative that you do so **at the beginning of your implementation** so that your entire website is within the scope of the worker. The [JavaScript snippet](#add-javascript-snippet-to-web-pages) that you added to your site pages contains a URL for `push-worker.js` and assumes it is placed in the root. **If you are unable to place the file at root, your entire site may not be able to access the service.** If you need to combine the push worker with an existing service worker, you may need to specify a new location for your worker file. You can do this by adding a `workerUrl: '/service-worker.js',` setting to your on-page snippet, inside your service worker. For further reading on service workers, see these excellent primers from Google and Mozilla: * [Chrome Developers: Service worker overview](https://developer.chrome.com/docs/workbox/service-worker-overview/) * [Mozilla Developer Network: Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) ### Troubleshooting: Duplicate Web Notifications If for any reason you have moved the service worker, e.g., from a non-root location to the root directory, users may receive duplicate push notifications. This is because users may have a browser-cached service worker running even though the file has been moved, and subscriptions are tied to push worker location. If they register again with the new (or newly-moved) service worker, they will have a new channel ID. **Add a new push-worker.js in prior location of service worker to unsubscribe users from duplicate notifications** ```js self.onactivate = function (ev) { self.registration.pushManager.getSubscription().then(subscription => { if (subscription) { subscription.unsubscribe() } }) } ``` To help out in situations like this, we have provided a bit of code (think of it as a "cleanup" service worker) that can be placed at the location of the original service worker. The process here is simple. All you have to do is put this code in a `push-worker.js` file placed where your previous service worker lived. Once you've done this, browsers will detect the change and unsubscribe any subscriptions tied to that worker location. And don't worry, this will not affect your Airship channels or any registrations tied to any other service worker locations. ## Send Your First Push Notification Now that you have configured your project for web push and are able to register users, it's time to send a test notification. You can do this via either the dashboard or our API. To send a notification via the dashboard, see: * [Create a message](https://www.airship.com/docs/guides/messaging/messages/create/) and [Web content](https://www.airship.com/docs/guides/messaging/messages/content/web/). * [Create a Scene](https://www.airship.com/docs/guides/messaging/in-app-experiences/scenes/create/) To send a web push via our API, two examples are provided below, one only to web browsers, and one to web, iOS, and Android. ### Web-Only Push In this example, we will introduce the [push object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#pushobject), the required request body for sending a web push notification via the push API, using the three required attributes `audience`, `device_types`, and `notification`. Audience : We'll start with the simplest [audience selector](https://www.airship.com/docs/developer/rest-api/ua/schemas/audience-selection/#audienceselector1000) possible: an individual channel ID. For testing purposes, you can retrieve your channel ID from a registered browser by entering the following in the JavaScript console: ```js const sdk = await UA const channelId = await sdk.channel.id() console.log("Channel ID: %s", channelId) ``` Device Types : Since we are only sending to web devices, we will specify the `"web"` device type as an array of one. In the next example, we will include additional device types. Notification : Finally, we will specify the values for our test notification, including the alert text, and web push-specific attributes that we will include in the [web platform override](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#weboverrideobject) object. Platform overrides can be used either to override a value for a specific platform or to specify values that are only applicable to that platform. **Example Request: Web only push** ```http POST /api/push HTTP/1.1 Authorization: Basic Content-Type: application/json Accept: application/vnd.urbanairship+json; version=3 { "audience": { "channel": "" }, "device_types": [ "web" ], "notification": { "alert": "Hello, Web!", "web": { "title": "My First Web Push Title", "require_interaction": true, "icon": { "url": "https://example.com/icon.png" } } } } ``` ### Push to Multiple Platforms This example illustrates sending a web push notification to a named user, "sven_svensson", who is registered on multiple platforms, including web. In the top-level `notification` attribute of the payload, the value for the `alert` property is "Hello, World!". Since we want our web users to experience the most relevant content possible, we are going to use the `web` override on the `notification` object to specify the more appropriate "Hello, Web!" alert message for web users. **Example Request: Push to multiple platforms** ```http POST /api/push HTTP/1.1 Authorization: Basic Content-Type: application/json Accept: application/vnd.urbanairship+json; version=3 { "audience": { "named_user": "sven_svensson" }, "device_types": [ "web", "ios", "android" ], "notification": { "alert": "Hello, World!", "web": { "alert": "Hello, Web!", "title": "My First Web Push Title", "require_interaction": true, "icon": { "url": "https://example.com/icon.png" } } } } ``` ### Additional Resources * [Web Content](https://www.airship.com/docs/guides/messaging/messages/content/web/) * [Web Platform Overrides](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#weboverrideobject) ## Web SDK This section is an introduction to the Web SDK and associated methods. Please visit our [Airship Web SDK Reference](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/index.html) for a complete reference. ### UaSDK The main Web SDK object. It cannot be instantiated manually. It is returned by the async loader. #### Creating a Channel You may create a channel by using the Airship SDK's `create` method; using this method will make the current browser known to Airship, and allow you to use the SDK features. ```js const sdk = await UA await sdk.create() ``` #### Registering for Push Notifications Follow these steps to register the current browser with Airship. * Fetch the browser's subscription object, prompting the user for permission if necessary. * Collect browser information for out-of-the-box tag segmentation. * Register with Airship and resolve the returned channel object. * Resolves an error that can be caught if there is an error while registering or if the browser is in an unsupported context. > **Note:** A request to register for push notifications _must_ happen as the result of a > user interaction, otherwise the registration will be rejected by the browser > without prompting the user. **Example registration** ```js const sdk = await UA // now that the SDK is available, add a registration button to the DOM const button = document.createElement('button') button.innerHTML = "Register for Notifications" button.onclick = async (el) => { await sdk.register() } document.body.append(button) ``` #### Checking Browser Capabilities Make sure that the current browser has web push features AND is in a secure context capable of attempting registration. - `sdk.isSupported` is `true` if the SDK can load and track events; web push support is not considered - `sdk.isWebPushSupported` is `true` if the browser is supported _and_ supports web push; it does not consider if the current _context_ is supported (such as requiring it be added to the home screen on iOS) - `sdk.isAllowedPushRegistrationContext` is `true` if the browser is currently in an allowed registration context; note this does not otherwise check for browser features, so should only be checked after an `sdk.isWebPushSupported` check - `sdk.canRegister()` is a comprehensive check against _all_ checks required for web push registration, and returns a promise that resolves to `true` if: - the browser supports web push - the page is a secure context - the browser is in an allowable context (such as being saved to the home screen on iOS) - push permission has not already been denied Using these methods may be important if you wish to support Safari on iOS or iPad OS, as those have special requirements before push notification registration is allowed: **Checking for Push Notification Registration Support** ```js const permission = await sdk.getNotificationPermission() if (permission === 'granted') { await sdk.register() } if (permission === 'denied') { // user has denied notifications at the browser level, and opt-in is not // possible without the user changing their browser settings } else if (sdk.isWebPushSupported && !sdk.isAllowedPushRegistrationContext) { // prompt user to add the website to their home screen } else if (await sdk.canRegister()) { // is in an allowed context; prompt the user to opt into push notifications } ``` ### Channel Object {#channel-object} The property `sdk.channel` returns the channel interface for the current browser. It contains methods for retrieving or setting information on the channel. **Example** ```js const sdk = await UA // the current channel id, a string. will be `null` if no channel exists const channelId = await sdk.channel.id() // the current opted-in status, a boolean const optedIn = await sdk.channel.optedIn() // opt out the channel for push notifications const optedIn = await sdk.channel.optOut() ``` ### Event Listeners The channel interface fires a `channel` event when a channel is loaded or registered. > **Note:** If you intend to set attributes, tags, named user, or anything that could modify > the channel within your event listener, make sure to set the `once` option to > `true` in order to prevent an infinite loop. ```js const sdk = await UA sdk.channel.addEventListener('channel', ev => { console.log('channel id:', ev.detail.id) }, { once: true }) ``` If you are on a page that is in-scope of your push-worker.js: The main SDK object fires a `push` event when the browser receives a push. ```js const sdk = await UA sdk.addEventListener('push', ev => { // ev.detail is the push payload object }) ``` ## Apple Safari Web Push certificates If your [Web channel was already configured for Safari support](#airship-setup), you must maintain your existing Safari certificate to keep supporting existing users who have opted in to web notifications, even if/when they upgrade to Safari 16 or above. Otherwise, you will need to rebuild your audience. To create an Apple Safari Website Push certificate, you will need: * An Apple Developer account * A Certificate Signing Request (CSR). If you don't already have this file saved locally, [follow Apple's instructions](https://developer.apple.com/help/account/create-certificates/create-a-certificate-signing-request) to create one. First, download your SSL certificate: 1. Log in to your [Apple Developer account](https://developer.apple.com/account). 1. Go to **Account**, then **Certificates, Identifiers & Profiles**. 1. Select **Identifiers**, select the **Website Push IDs** filter, and then select your web push identifier from the list. 1. Select **Create Certificate** under **Production Certificates**. This will generate a Website Push Notification service SSL (Sandbox & Production) certificate compatible with both the Production and Development environments. 1. Select **Choose File** and upload your CSR file, and then select **Continue**. 1. Select **Download** and save the SSL certificate file for use in the next step. You may need to reload the page if the Download button is not yet active. Next, install your certificate, export it as a .p12 file, and create a password: 1. Open the certificate you downloaded in the previous step. It should open in the Keychain Access app and be listed in **My Certificates**. 1. ![Getting Started](https://www.airship.com/docs/images/p-export-p12_hu_853f430fb03d4b60.webp) Select the certificate in the list, then go to the **File** menu and select **Export Items...**.

Also be sure to select the **My Certificates** category in the sidebar. If **My Certificates** is not highlighted, you will not be able to export the certificate as a .p12 file. 1. ![Getting Started](https://www.airship.com/docs/images/export-filename_hu_a67ac4824a950386.webp) Save the file in the Personal Information Exchange (.p12) format.

You will be prompted to create a certificate password, which you will enter when you upload the .p12 file in the Airship dashboard. Now you can upload the .p12 file and provide the password when [configuring the Web channel in the Airship dashboard](#airship-setup). # Implementing Web SDK v2 > Implement version 2 of the Airship Web SDK.The v2 Web SDK supports features not included in v1: * Web [Scenes](https://www.airship.com/docs/reference/glossary/#scene), including survey questions and Story format * Registering browsers without Push Notification Opt-in ## Removed Support and Known Issues Before continuing, confirm that you do not use or require the following features of the v1 SDK, as support for them has been removed: * **HTTP setups** — This is also referred to as "secure bridge." The v2 SDK requires HTTPS on all pages. We removed the registration page plugin, as it was only needed for these setups. * **Multi-domain setups** — This is where a single registration was shared across multiple domains or subdomains. * **Safari versions older than 16 (APNs Safari)** — Apple started supporting VAPID push in Safari 16 on macOS 13 and above, which was released October 2022. * **AMP** — We will continue to support Web configurations that previously had Accelerated Mobile Pages (AMP) enabled, but AMP is not supported for new configurations. Additionally, APNs Safari registrations are not automatically migrated to VAPID. They must be re-registered using the `sdk.register()` method. ## Integration Follow the below instructions depending on whether you are performing a new Installation or upgrade. ### Setting Up a New Integration If you have not previously integrated the Airship Web SDK, follow these steps in the Web *Getting Started* guide: * [Airship Setup](https://www.airship.com/docs/developer/sdk-integration/web/getting-started/#airship-setup) * [Add JavaScript Snippet to Web Pages](https://www.airship.com/docs/developer/sdk-integration/web/getting-started/#add-javascript-snippet-to-web-pages) * [Place Service Worker](https://www.airship.com/docs/developer/sdk-integration/web/getting-started/#place-service-worker) Then proceed to [Channel Registration](#channel-registration) below. ### Updating an Existing Integration > **Important:** The v2 SDK has an updated API which is incompatible with the previous version. > An upgrade must be performed in a single release, you cannot partially upgrade. > > Airship has not thoroughly tested downgrading from v2 to v1. It should be > assumed that a downgrade is not possible once you have updated your > production site to v2. Follow these steps in the Web *Getting Started* guide to redownload your SDK bundle and replace your existing JavaScript snippets and service worker with the bundle's updated contents: * [Airship Setup](https://www.airship.com/docs/developer/sdk-integration/web/getting-started/#airship-setup) * [Add JavaScript Snippet to Web Pages](https://www.airship.com/docs/developer/sdk-integration/web/getting-started/#add-javascript-snippet-to-web-pages) * [Place Service Worker](https://www.airship.com/docs/developer/sdk-integration/web/getting-started/#place-service-worker) Also follow our [Developer Migration Guide](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/tutorial-v1-to-v2-migration.html) , which instructs you on migrating your integration to the updated API. Then proceed to Channel Registration below. ## Channel Registration To use the new features of the v2 Web SDK, you will need to register a channel for browsers that visit your web site. Channel registration no longer requires push notification opt-in. When you choose to do this will be up to your integration, but Airship recommends registering a channel only once a visitor is considered relevant to you. > **Note:** Any visitor you choose to register will count toward your [Monthly Unique > Visitors](https://www.airship.com/docs/reference/billing/#monthly-unique-visitors). If your > billing plan does not include Monthly Unique Visitors, you will be contacted by > your Account Manager to update your subscription. The following methods are relevant to channel registration: * [UaSDK.create](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/UaSDK.html#create) —  Create a channel without prompting for notification opt-in. * [UaSDK.register](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/UaSDK.html#register) —  Create a channel prompting for notification opt-in. If the user denies notification permissions, has previously denied it, or if the browser is in an unsupported context this method will throw an error, and not register a channel. See the example below for details on how to approach a push notification registration. **Registering a channel without notification opt-in** ```js const sdk = await UA const {channelId} = await sdk.create() console.debug('registered channel with id:', channelId) ``` You may decide to register that channel for push notifications at a later date using the `register()` method, as usual. Note that if you are calling `register()` you **do not** need to also call `create()`: **Requesting push notification opt-in** ```js const sdk = await UA if (sdk.isWebPushSupported && sdk.isAllowedPushRegistrationContext) { // prompt the user to install to their home screen so notification // registration may be requested. } else if (await sdk.canRegister()) { const {channelId} = await sdk.register() console.debug('opted in push notifications for channel with id:', channelId) } ``` ## Using the v2 SDK and Instrumenting Your Application For detailed v2 SDK usage information, see the [API Documentation](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/index.html) . To get the most out of the v2 SDK, we recommend instrumenting your website or web application to emit events for triggering and event segmentation. See [Instrument your Application](https://www.airship.com/docs/developer/sdk-integration/web/in-app-experiences/#instrumenting-your-application) in the *In-App Experiences* documentation. ## Custom Domain Proxy If you intend to use Web [Scenes](https://www.airship.com/docs/reference/glossary/#scene), you should send Airship traffic through your own domain in order to provide a consistent brand experience and avoid blocked content. See [Custom Domain Proxy](https://www.airship.com/docs/guides/getting-started/developers/custom-domain-proxy/). # In-App Experiences > Instrument your website or web application for in-app experiences. {{< badge_sdk_min web="2+" >}}The Airship Web SDK v2 supports [Scenes](https://www.airship.com/docs/reference/glossary/#scene), including [Embedded Content](https://www.airship.com/docs/developer/sdk-integration/web/embedded/). See also [Implementing Web SDK v2 ](https://www.airship.com/docs/developer/sdk-integration/web/v2-sdk/). ## Instrumenting your Application In order for Scenes to function, you must [Create a channel](https://www.airship.com/docs/developer/sdk-integration/web/getting-started/#creating-a-channel). This will count the current browser toward your [Monthly Unique Visitors](https://www.airship.com/docs/reference/billing/#monthly-unique-visitors). To get the most out of Scenes, instrument your website or web application to emit events for triggering and event segmentation. The following sections describe the events and provide code samples. See also: * [In-App Experience Triggers](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/triggers/) * [Event segmentation](https://www.airship.com/docs/developer/rest-api/ua/schemas/event-segmentation/) in our API reference ### Custom Events [Custom Events](https://www.airship.com/docs/reference/glossary/#custom_event) can be used to track any interaction you wish, for example a purchase or a product view. **Sending a Custom Event** ```js const sdk = await UA const evt = new sdk.CustomEvent('purchase', 34.5, {productId: 1337}) await evt.track() ``` ### Screen View Events Tracking App Screens can make it easy to trigger a Scene or to ensure a Scene is only displayed when currently on a given screen. You must [define App Screens](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/app-screens/) in the Airship dashboard. In the context of a website or web application, you can think of a "screen" as a page with a general purpose. For example, `home`, `product_listing`, and `product_detail` are good generic screen names, whereas `product_id_18957` is too specific and unlikely to be generally useful for reporting or targeting. **Tracking a Screen View** ```js const sdk = await UA await sdk.analytics.trackScreen('home') ``` ### Feature Flag Interactions When using [Feature Flags](https://www.airship.com/docs/reference/glossary/#feature_flag), you should track when a user has interacted with the feature the flag controls. The interaction event is included in Feature Flag reporting and can also be used for triggering: **Tracking Interaction with a Flagged Feature** ```js const sdk = await UA const flag = await sdk.components.featureFlags.get('new_product_pages') // later, when the user interacts with the new product pages await sdk.components.featureFlags.trackInteraction(flag) ``` ### App Version Updates If you version your web app and wish to be able to target users who may have seen an earlier version of the app, when initializing the SDK, use the `appVersion` property set to your app's version number. This will be passed in your snippet as well as your push worker: **Setting your App Version** ```js ``` ## Controlling Scenes If Scenes are enabled for your project, they will display according to their trigger and conditions settings. You can control their display programmatically or disable them completely, if desired. ### Screen Sizes and Breakpoints When creating Scene [view styles](https://www.airship.com/docs/guides/features/messaging/scenes/scenes/#view-styles) in your project's default settings, you can [define alternative settings that apply to a Scene based on a device's screen size](https://www.airship.com/docs/guides/features/messaging/scenes/conditional-design-overrides/). These sizes are defined by screen width breakpoints in the SDK: * Small: Less than 1024px * Medium: Greater or equal to 1024px and less than 1920px * Large: 1920px and greater You can override the values by setting custom breakpoints that better match your web application. Pass the following options in your SDK initialization snippet: **Setting Screen Sizes** ```js components: { inAppAutomation: { displayBreakpoints: { // the size at which a screen will be considered "medium", in pixels; inclusive medium: 800, // the size at which a screen will be considered "large", in pixels; inclusive large: 1024 } } } ``` Anything smaller than "medium" is considered to be a "small" screen, so no value needs to be set for that breakpoint. If you set custom breakpoints, also add them to your project settings for more accurate device previews when creating Scenes. Follow the steps for [Setting Scene defaults](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/defaults/#scene-defaults) in *In-App Experience Defaults* and set Breakpoint values in a view style's Background settings. ### Disabling To permanently disable Scenes in the current browser, pass the following options in your SDK initialization snippet: **Disabling Scenes** ```js components: { inAppAutomation: { enabled: false } } ``` ### Pausing and Resuming Display You can choose to pause and resume Scene display. For example, you might wish to only disable showing new Scenes when opening a modal on your site or during a checkout flow. **Pausing and Resuming Scenes** ```js const sdk = await UA // pause display of scenes await sdk.components.inAppAutomation.setPaused(true) // get the paused status; resolves to a boolean value await sdk.components.inAppAutomation.isPaused() // resume display await sdk.components.inAppAutomation.setPaused(false) ``` By default, displays are not paused. If you'd like to start in a paused state so that you can manually unpause at a time of your choosing, pass the following configuration in your SDK initialization snippet: **Starting Scenes in Paused State** ```js components: { inAppAutomation: { startInPausedState: true } } ``` ### Display Interval The display interval controls the amount of time to wait before the manager can display the next triggered Scene. The default value is set to 0 milliseconds and can be adjusted to any amount of time in milliseconds. **Setting the Display Interval Programmatically** ```js await sdk.components.inAppAutomation.setDisplayInterval(30000) // 30 seconds ``` You can also set the display interval via your SDK initialization snippet: **Setting the Display Interval via Config** ```js components: { inAppAutomation: { displayIntervalMs: 30000 // 30 seconds } } ``` ### Delegating Display You can choose to implement your own display delegate, which can control if a Scene is allowed to be displayed. It can also be notified when a Scene is displayed or has finished displaying. Your display delegate must implement the `[InAppDisplayDelegate](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/InAppDisplayDelegate.html) ` interface. All methods are optional. > **Note:** When using a display delegate, we recommended that you [start display in a paused state](#pausing-and-resuming-display) and then unpause after setting your delegate. > Otherwise, it is not guaranteed your delegate will be registered before displays > occur. **Controlling Display using a Delegate** ```js const delegate = { isMessageReadyToDisplay: (message) => { if (myApp.canDisplayMessage(message)) { return true } return false } } await sdk.components.inAppAutomation.setDisplayDelegate(delegate) await sdk.components.inAppAutomation.setPaused(false) ``` If you need to pass additional information to your application, you can use [Custom Keys](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/optional-features/#custom-keys) when composing your Scene. They will be available on the `extras` property of the `[InAppAutomationDetails](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/global.html#InAppAutomationDetails) ` that are passed to your delegate. ### Dark Mode By default, Scenes follow the browser preferences for dark mode. This behavior can be disabled by a configuration value. When disabled, all displayed content will use the colors configured for light mode. **Disabling Automatic Dark Mode** ```js components: { inAppAutomation: { matchBrowserDarkMode: false } } ``` ## Custom Views A *Custom View* is a native view from your mobile or web application embedded into a Scene. Custom Views can display any native content your app exposes, so you can reuse that existing content within any screen in a Scene. For a web page, this means embedding an HTML view into a Scene. > **Note:** When using Custom Views, you should initialize In-App Experiences in a paused > state. If you do not, there's no guarantee that your Custom View delegate will > be registered by the time a Scene displays, and the Custom View would not be > shown. See the section on [Pausing and Resuming > Display](#pausing-and-resuming-display) for further information. ### Implementation To display Custom Views, you must implement the `[InAppCustomViewDelegate](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/InAppCustomViewDelegate.html) ` interface and register it using the `[setCustomViewDelegate](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/InAppAutomationManager.html#setCustomViewDelegate) ` method of the `[InAppAutomationManager](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/InAppAutomationManager.html) `. Only one delegate may be registered at a time. When it's time for a Custom View to be displayed, your delegate's `onCustomViewShown` method will be called with the following parameters: * `name` — The name of the Custom View, which is referenced when adding it to a Scene * `element` — The HTML element into which your Custom View should render * `properties` — Any additional properties set for your Custom View as a key/value object; all keys and values will be strings When called, your `onCustomViewShown` method must return an object that fulfills the `[InAppCustomViewHandler](https://www.airship.com/docs/reference/libraries/web-notify-sdk/v2-latest/InAppCustomViewHandler.html) ` interface. This will contain the `destroy` method, which will be called with no parameters when your view is to be removed. > **Important:** Your delegate should render synchronously into the provided `element`. If it > does not, you may cause the Scene's layout to shift after rendering. ### Example Custom View The following example shows a Custom View that renders an embedded [Google Map](https://developers.google.com/maps/documentation/embed/embedding-map) when called to render a Custom View named `map`. This example assumes that you have started In-App Experiences in a paused state, as recommended above. In our example, we pass `properties` to the view to determine the location the map should show. `q` is the query to pass to the map view. **Example Custom View rendering a map** ```js // a React Function Component for rendering an embedded Google Map const Map = ({ q }) => { return (