v1-to-v2-migration

Migrating from SDK v1 to v2

This document covers any code changes you'll need to make when migrating for the v1 Airship Web SDK to v2, as well as covering new features you may wish to implement while making updates.

The changes are minimal, and for most integrations the upgrade will not involve more than updating some existing method calls to their new names or locations. In some cases calls have been simplified to reduce the amount of asynchronous waiting you must do before using an interface.

Removed Support

Some features were removed from the SDK; if you still rely on these features, then you will wish to stay on the v1 SDK:

  • Support for HTTP setups; this is also referred to as "secure bridge". The v2 SDK requires HTTPS on all pages.
    • The registration page plugin has been removed, as it was only needed for these setups
  • Multi-domain setups are not supported; this is where a single registration was shared across multiple domains or subdomains.
  • APNs Safari is no longer supported, as Apple started supporting VAPID push in Safari 16 on macOS 13 and above, which was released October 2022.

Known Issues

  • APNs Safari registrations are not automatically migrated to VAPID, and must be re-registered using the sdk.register() method.
  • AMP support is untested

New Features

The following new features were added:

Built for ES6/ES2015

The Airship SDK has dropped ES5 support, resulting in a significant reduction in the SDK's initial load and parse times.

Registering Browsers without Push Notification Opt-in

It's now possible to register a browser without requesting notification opt-in, so you can track events, set tags and attributes, and display In-App Experiences to your web audience. A new method sdk.create() has been added as a sibling to the existing sdk.register(), and will create a channel for the browser but not prompt for browser notification access. At a later date you may still call sdk.register() to request notification access.

const {channelId} = await sdk.create()
console.log('registered a channel', channelId)

In-App Experiences

In-App Experiences are now supported on web devices, so you can display scenes, surveys, and stories to your users. This feature is enabled by default in the SDK and does not require any further integration for support. However you may wish to track additional events to support triggering of these experiences.

// a custom event
const event = new sdk.CustomEvent('ate_food', 3.5, {meal: 'pancakes'})
await event.track()
// screen view events
await sdk.analytics.trackScreen('home')

Changes Required

The following changes are required when migrating to the v2 SDK; this includes methods which have changed or moved, as well as methods that have been deprecated and removed.

Legacy Initialization Support Removed

Very early implementers of the Airship SDK had a different initialization snippet from later versions. Support for this early snippet has been dropped. The deprecated snippet will appear similar to the following:

!function(n,t,c,e,u){function r(n){try{f=n(u)}catch(n){return h=n,void i(p,n)}i(s,f)}function i(n,t){for(var c=0;c<n.length;c++)d(n[c],t);
}function o(n,t){return n&&(f?d(n,f):s.push(n)),t&&(h?d(t,h):p.push(t)),l}function a(n){return o(!1,n)}function d(t,c){
n.setTimeout(function(){t(c)},0)}var f,h,s=[],p=[],l={then:o,catch:a,_setup:r};n[e]=l;var v=t.createElement("script");
v.src=c,v.async=!0,v.id="_uasdk",v.rel=e,t.head.appendChild(v)}(window,document,'https://aswpsdkus.com/notify/v1/ua-sdk.min.js',
  'UA', options)

Most projects will have the newer snippet, however if you have the earlier version you must update to the latest one.

The easiest way to determine which version you have:

  • If the string _async_setup appears in your snippet, you have the updated initialization and do not need to take any action.
  • If the string _setup appears (not prefixed by _async) you have an old version of the initialization and must update.

If you need to update your snippet, the best method is to log into your Airship dashboard and re-download the SDK bundle from your application.

Channel Tags, Attributes, and ID

The previous Channel tags and attributes interfaces have been deprecated, and new interfaces which mirror those available on the Contact interface. Any calls to sdk.channel.tags or sdk.channel.attributes must be updated to these new interfaces:

// tag editor
await sdk.channel
  .editTags()
  .add('device', 'cool-tag')
  .remove('device', 'uncool-tag')
  .apply()

// attribute editor
await sdk.channel
  .editAttributes()
  .set('first_name', 'Guy')
  .remove('birthday')
  .apply()

All operations are queued in the editor instance until apply() is called. If not called, any changes are discarded.

The channel ID is no longer synchronously available on the channel. See the migration guide later in this document for details.

Removal of "has tags" methods

The tags.has() methods were removed, as this method does not provide an accurate view of the actual channel or contact's tags. If your application requires tracking any segmentation data, we recommend storing that data within your application.

Chainable Tag, Attribute, and Subscription List Editors

As you saw in the section above, the tag and attributes interfaces are now chainable. This is also true of subscription lists:

await sdk.contact.subscriptions
  .edit()
  .subscribe('news')
  .unsubscribe('product_updates')
  .apply()

Locale

The locale override interface has been removed, and replaced with a new interface. Any calls to sdk.localeOverride must be updated:

await sdk.locale.set({language: 'fr', country: 'FR'})

Simplified Asynchronous Interfaces

The following interfaces previously required awaiting the promise they returned before they could be used, but are now synchronously available, simplifying their use:

  • sdk.channel.subscriptions
  • sdk.contact
  • sdk.contact.editTags
  • sdk.contact.editAttributes
  • sdk.contact.subscriptions

Any previous code where you were await-ing those methods or calling .then on them should be updated.

// listing channel subscriptions
const lists = sdk.channel.subscriptions.list()
// updating contact tags
await sdk.contact
  .editTags()
  .set('crm', 'pro-user')
  .remove('interests', 'spelling')
  .apply()

Updates to SDK Support Methods

SDK support methods have been updated to better represent the available features in the browser, and to help you decide what actions you can take. See the "Support Methods" section of the upgrade guide below for details on the changes made.

Feature Flags and Preference Center Interfaces

The Feature Flags and Preference Center interfaces have moved; where they were previously at the top-level of the SDK, they are now under a components property:

// feature flags
const {eligible} = await sdk.components.featureFlags.get('new_feature')

// preference centers
const definition = await sdk.components.preferenceCenters.get('web_preferences')

Enabling and Disabling Data Collection

Previously, re-enabling data collection via sdk.setDataCollectionEnabled(true) would attempt to opt the channel back in to push notifications if the browser's notification permission was granted. However in some cases this wouldn't actually opt the browser in, but would cause the sdk.channel.optedIn() method to temporarily resolve to true, even though the browser was not actually opted in.

This method no longer attempts to opt the channel back in, as we do not know if the channel should be opted in. Instead, if you wish to opt the channel back in after re-enabling data collection, call the register() method after checking permission is granted:

await sdk.setDataCollectionEnabled(true)
const permission = await sdk.getNotificationPermission()
if (permission === 'granted') {
  await sdk.register()
}

Minor Changes

  • sdk.disableAnalytics is no longer available, use the asynchronous methods sdk.analytics.isEnabled and sdk.analytics.setEnabled
  • sdk.isGestureRequired has been removed as all browsers now recommend only performing a notification opt-in as the result of a gesture
  • sdk.permission has been removed, use the asynchronous method sdk.getNotificationPermission() to check notification permissions

Upgrade Guide

Important: the steps in this upgrade guide must be performed in a single release; there is no "partial" upgrade supported.

Update your SDK initialization

Note: Before starting, read the above "Legacy Initialization Support Removed" section of this document, as it contains important information that will affect a small number of early adopters of the Airship Web SDK. This section assumes you've already updated to the latest snippet.

Your initialization snippet (typically in your site's HTML template, and in the push worker) will contain a reference to the location of the Airship SDK's source URL; it will look something like:

https://aswpsdkus.com/notify/v1/ua-sdk.min.js

…and becomes similar to:

https://aswpsdkus.com/notify/v2/ua-sdk.min.js

This will vary based on your data center and features you are using, however it should be updated to the v2 SDK location. In occurrences in both your HTML initialization snippet and your push worker (typically located at push-worker.js). Important: do not forget to update the push worker!

Update to the New API

There were a small set of changes to the SDK's public interface which must be updated in your code; we'll cover each of the items individually, giving an indication of what you should be searching for and examples of how you'd update the calls.

Channel ID

If you were relying on the value channel.id, you must update how you retrieve that value as it is no longer synchronously available. Calls to the old interface will look like:

  • channel.id

This is now a method channel.id() which will resolve to either the Channel ID, or null if one is not available. In cases where you must be notified of the ID when available, you can use the following methods:

// the `channel` event is emit once per SDK initialization when the channel is
// loaded or registered
sdk.channel.addEventListener('channel', ({detail}) => {
  console.log('channel id is %s', detail.id)
})
// channel is available on registration
const {channelId} = await channel.register()
// or on create
const {channelId} = await channel.create()

It's generally recommended you store the result of registering or creating a channel should you need the ID for integration purposes.

Channel Tags

If you're making any calls to the channel.tags interface, you must update these to the new TagEditor. Calls to the old interface will look like:

  • channel.tags.add('likes-food')
  • channel.tags.remove('dislikes-food')
  • channel.tags.add('sauerkraut', 'foods')

These calls will be updated to use the TagEditor available on the Channel. They will look something like:

await sdk.channel
  .editTags()
  .add('device', 'likes-food')
  .remove('device', 'dislikes-food')
  .add('foods', 'sauerkraut')
  .apply()

This interface has the benefit of all tags being applied at once when the apply method is called.

Note: Previously, the device tag group was implicit when omitting the second parameter to the tag add/remove/set methods. It is now explicitly required. The tag group is always the first parameter to the TagEditor methods, where previously it was the optional second parameter.

Warning: The tags has method has been removed, and there is no replacement method. If you require tracking of segmentation data within your application, we suggest moving that data into your application's storage.

Channel Attributes

If you're making any calls to the channel.attributes interface, you must update these to the new AttributeEditor. Calls to the old interface would look like:

  • channel.attributes.set('first_name', 'Guy')
  • channel.attributes.remove('industry')

These calls will be updated to use the AttributeEditor available on the UaSDK.Channel. They will look something like:

await sdk.channel
  .editAttributes()
  .set('first_name', 'Guy')
  .remove('industry')
  .apply()

This interface has the benefit of all attribute mutations being applied at once when the apply method is called.

Channel Subscription Lists

The subscription lists editor for the channel no longer needs to wait for a promise to resolve, and supports chaining. If you were using the editor, it would have looked something like:

// using await
const editor = await sdk.channel.subscriptions.edit()
editor.subscribe('cool-list')
editor.unsubscribe('uncool-list')
await editor.apply()
// using .then
sdk.channel.subscriptions
  .edit()
  .then((editor) => {
    editor.subscribe('cool-list')
    editor.unsubscribe('uncool-list')
    return editor.apply()
  })
  .then(() => console.log('changes applied'))

The edit() method no longer returns a promise, but instead a ready-to-use SubscriptionListEditor. Any calls may be updated to use the editor immediately:

await sdk.channel.subscriptions
  .edit()
  .subscribe('cool-list')
  .unsubscribe('uncool-list')
  .apply()

Named User Interface

The named user interface available at channel.namedUser has been removed in favor of the UaSDK.Contact interface. If you were using the old interface, you would have calls that look like:

  • channel.namedUser.set('cool-user')
  • channel.namedUser.remove()

These will be updated to use the UaSDK.Contact interface, which has matching calls for the above:

// replaces the `set` method on named user
await sdk.contact.identify('cool-user')
// replaces the `remove` method on named user
await sdk.contact.remove()

Similar to the above for the channel, there were interfaces for editing named user tags and attributes; we won't re-hash all of the changes, but they will be calls similar to:

  • channel.namedUser.tags.add('sauerkraut', 'foods')
  • channel.namedUser.attributes.set('first_name', 'Guy')

These will be updated to use the TagEditor and AttributeEditor interfaces available on the UaSDK.Contact respectively:

// tags
await sdk.contact.editTags().add('foods', 'sauerkraut').apply()

// attributes
await sdk.contact.editAttributes().set('first_name', 'Guy').apply()
Removal of Named User ID retrieval methods

With the removal of channel.namedUser the channel.namedUser.id property was removed and has no equivalent replacement, as it did not necessarily provide an accurate view of the current Named User ID if it had been changed outside of the SDK. If your application requires tracking this ID, we recommend storing that data within your application.

Contact Interface

The UaSDK.Contact interface was previously only asynchronously available, as were its editor methods:

  • contact.editTags()
  • contact.editAttributes()
  • contact.subscriptions.edit()

If you were using any of these, you may have been awaiting them or calling .then on the promise they return. This is no longer required, nor is the sdk.contact required to be awaited.

Previously you may have had code like:

// with await
const contact = await sdk.contact
const editor = await contact.editTags()
editor.add('foods', 'sauerkraut')
await editor.apply()

// with .then
sdk.contact
  .then((contact) => contact.editTags())
  .then((editor) => {
    editor.add('foods', 'sauerkraut')
    return editor.apply()
  })
  .then(() => console.log('applied tags'))

…this has been greatly simplified in the latest SDK:

// with await
await sdk.contact.editTags().add('foods', 'sauerkraut').apply()

// with .then
sdk.contact
  .editTags()
  .add('foods', 'sauerkraut')
  .apply()
  .then(() => console.log('applied tags'))

Custom Event

Calls to the CustomEvent track() method previously would resolve to an object containing a boolean ok property, and would not give a reason why the call had failed. We now raise an UaSDK.Errors.ReportingDisabledError (or subclass thereof) if an error occurs due to reporting being disabled.

You should try/catch calls to the track() method:

const event = new sdk.CustomEvent('ate_food', 3.5, {meal: 'pancakes'})
try {
  await event.track()
} catch (e) {
  if (e instanceof sdk.errors.ReportingDisabledError) {
    console.warn('reporting is disabled')
    return
  }
  throw e
}

Locale Override

The sdk.localeOverride interface has been removed in favor of a simplified UaSDK.LocaleManager interface. Calls to the old interface would look like:

  • localeOverride.setCountry('FR')
  • localeOverride.setLanguage('fr')
  • localeOverride.set({language: 'fr', country: 'FR'})
  • localeOverride.clear()

Any usages of this previous localeOverride interface must be updated to use sdk.locale, which has a similar interface:

// setting just the country
await sdk.locale.set({country: 'FR'})
// setting just the language
await sdk.locale.set({language: 'fr'})
// setting both simultaneously
await sdk.locale.set({language: 'fr', country: 'FR'})
// clearing any set override
await sdk.locale.clear()

The localeOverride interface also had static properties which allowed accessing the currently set overrides:

  • localeOverride.language
  • localeOverride.country

These properties have been removed, and are replaced with an asynchronous get method which can retrieve the current resolved locale for the browser: unlike the older methods that only returned the override, this method returns the full locale for the current browser as an object with country and language properties. Note that in some browsers, country may be null:

const {country, language} = await sdk.locale.get()

Preference Centers

Optional SDK components have moved from the top-level SDK object to a components sub-object; this means any calls to retrieve preference centers via PreferenceCenterManager must be updated. Previously, calls would've looked like:

  • preferenceCenters.get()
  • preferenceCenters.list()

These will be updated to use the components object:

const definitions = await sdk.components.preferenceCenters.list()
const definition = await sdk.components.preferenceCenters.get('web_preferences')

Feature Flags

Optional SDK components have moved from the top-level SDK object to a components sub-object; this means any calls to evaluate feature flag membership via FeatureFlagManager must be updated. Previously, calls would've looked like:

  • featureFlags.get('new_feature')
  • featureFlags.trackInteraction(newFeatureFlag)

These calls must be updated to use the components object:

const flag = await sdk.components.featureFlags.get('new_feature')
// later, when a user has interacted with the feature
await sdk.components.featureFlags.trackInteraction(flag)

Disable Analytics Flag

Previously, there was a property on the SDK which acted as a getter and setter for disabling analytics. These calls would look like:

  • sdk.disableAnalytics

This property has been removed in favor of separate asynchronous getter and setter methods, and usages must be updated. Note: This method is the inverse of the method it replaces to better match other SDK APIs!

const enabled = await sdk.analytics.isEnabled()
// disable analytics
await sdk.analytics.setEnabled(false)
// enable analytics
await sdk.analytics.setEnabled(true)

Data Collection Flag

Previously, there was a property on the SDK which acted as a getter and setter for disabling data collection. These calls would look like:

  • sdk.dataCollectionEnabled

This property has been removed in favor of separate asynchronous getter and setter methods, and usages must be updated:

const enabled = await sdk.isDataCollectionEnabled()
// disable data collection
await sdk.setDataCollectionEnabled(false)
// enable data collection
await sdk.setDataCollectionEnabled(true)

Additionally, if you use these methods the previous SDK had a behavior which would partially opt in the channel if data collection was toggled back on. This behavior has been removed, and if you wish to opt a channel back in you should use the sdk.register() method to opt the channel back in to push after checking the permission:

await sdk.setDataCollectionEnabled(true)
const permission = await sdk.getNotificationPermission()
if (permission === 'granted') {
  await sdk.register()
}

Notification Permission Property

Previously, there was a property on the SDK which would determine the current browser permission for notifications. Calls to this property would look like:

  • sdk.permission

This property has been removed in favor of an asynchronous method which will be more accurate in a wider variety of cases.

const permission = await sdk.getNotificationPermission()
if (permission === 'denied') {
  // permission is denied at the browser level
}

Note: this new method returns a slightly different return value; previously the value default would be returned if the permission was in its default state (neither granted nor denied). Now the value prompt is returned for consistency with the PermissionStatus interface.

Can Register Method

Previously, there was a property on the SDK which would determine if the browser was currently able to register. Calls to this property would look like:

  • sdk.canRegister

This has been updated to an asynchronous method which will return an accurate value in a wider variety of cases.

if (await sdk.canRegister()) {
  // ask user if they'd like to opt in to notifications
}

Gesture Required

Previously, there was an SDK property which could be checked to determine if push notification registration was required to be called as the result of a user gesture. Calls would look like:

  • sdk.isGestureRequired

This property has been removed as all vendors and best practices now state that notification opt-in must be done as a reaction to a gesture.

Support Methods

The following support properties and methods have had their meanings updated to better match the SDKs new features:

  • 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 ensures:
    • 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

In general, you will not need to change how you were using these methods; however if your app is expected to support Safari on iOS, you may wish to ensure you are checking for that support in the following way:

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
}

Test and Deploy

Once you've updated API calls, you are ready to test in your staging environment, and should everything work as expected deploy to production.

Your end users will not notice the change from the old to new SDK, and will be seamlessly transitioned from the v1 to the v2 SDK upon visit. It will not affect their ability to receive notifications if they've not yet visited.

Full List of Changes

  • The SDK now supports creating a channel for browsers without notification opt-in, using sdk.create(). You may still request notification permission at a later date with sdk.register()
  • Legacy channel interfaces have been removed for tags, attributes, and named user:
    • channel.tags has been replaced by the tag editor at channel.editTags()
    • channel.attributes has been replaced by the attribute editor at channel.editAttributes()
    • channel.namedUser has been replaced by the contact interface
  • The channel id is no longer synchronously available at channel.id; this is now an asynchronous method channel.id() to retrieve it when available.
  • The legacy synchronous loader has been removed; only the _async_setup initialization snippet is supported
  • Many interfaces that required awaiting a promise to resolve are now available synchronously for easier use:
    • The SDK's contact interface at sdk.contact
    • The contact's tags, subscription lists, and attribute editors available at:
      • sdk.contact.editTags()
      • sdk.contact.editAttributes()
      • sdk.contact.subscriptions.edit()
    • The channel subscription list editor, available at sdk.channel.subscriptions.edit()
  • Calls to the track method of the CustomEvent class will now throw sdk.errors.ReportingDisabledError (or a subclass thereof) if event reporting is disabled due to analytics or SDK data collection being disabled.
  • sdk.dataCollectionEnabled is no longer available, use the asynchronous methods:
    • sdk.isDataCollectionEnabled to get the current setting
    • sdk.setDataCollectionEnabled to update the setting
  • sdk.localeOverride moves to sdk.locale, and has an updated interface
    • All calls change to sdk.locale.set
      • sdk.localeOverride.setCountry('DE') becomes sdk.locale.set({country: 'DE'}) and similarly for language
    • Current locale no longer available as synchronous properties on the locale, instead use await sdk.locale.get() which returns the resolved locale, not just overrides
  • sdk.disableAnalytics is no longer available, use the asynchronous methods:
    • sdk.analytics.isEnabled to get the current setting
    • sdk.analytics.setEnabled to update the setting
  • sdk.isGestureRequired has been removed as all browsers now recommend only performing a notification opt-in as the result of a gesture
  • Enabling data collection via sdk.setDataCollectionEnabled will no longer implicitly opt channels back in if the browser notification permission is granted, as we cannot know for certain if a recorded opt-out was due to data collection being disabled or some other mechanism (such as an explicit call to sdk.channel.optOut()). If you wish to opt the user back in, you may check the sdk.permission method and determine if sdk.register() should be called again
  • sdk.permission has been removed, use the asynchronous method sdk.getNotificationPermission() to check notification permissions
  • sdk.canRegister has been changed to an asynchronous method rather than a static property
  • SDK support methods have been updated to better represent the available features in the browser:
    • 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 ensures:
      • 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
  • Optional components have moved to sdk.components:
    • sdk.preferenceCenters moves to sdk.components.preferenceCenters
    • sdk.featureFlags moves to sdk.components.featureFlags