Movable Ink

AIRSHIP MAINTAINED INTEGRATION

This integration is maintained by Airship. Please contact Airship for support.

Overview and Setup

Movable Ink helps you deliver highly relevant, personalized push notifications, Message Center messages, and in-app automations.

Contact alliances@airship.com to get started with Movable Ink in Airship. Airship will help you set up your integration and create Message Center templates so you can use your Movable Ink campaigns in Airship.

To use Movable Ink with Airship, you must use your audience’s channel_id or named_user_id identifiers as Movable Ink merge tags — the mi_u query parameter in Movable Ink. This helps ensure that each member of your audience receives personalized content.

To take advantage of Movable Ink, you’ll insert blocks of content or Creative Tags from Movable Ink into your Airship messages. A Creative Tag is a snippet of HTML that includes business logic to render personalized, intelligent content in your Airship message.

Use Cases

Movable Ink helps you personalize the look and feel of your message to suit each individual in your audience.

  • Merge sale-specific information in your messages: Promote product-based messaging in your app after a user views content in a website or an email.

  • Target your audience contextually: Send time-targeted messages to make sure your audience knows about upcoming events, weather, time-targeted offers, etc.

  • Offer behavioral recommendations: Let your audience know about product substitutes or events tangential to their current interests to keep them engaged.

Movable Ink Support in Airship

Movable Ink dynamically generates creative content in real-time for your mobile app using Airship. This table lists many of the Movable Ink capabilities available for Airship apps.

 Note

If a Movable Ink capability is not listed in this table, then it is fully supported for mobile messaging with Airship.

Movable Ink capabilityRich push notificationIn-App Automation/Message CenterNotes
Creative OptimizerDisplay A/B content
OptimizeUse the Channel ID to optimize. Must deep link so Movable Ink can capture clicks and optimize the content. Cannot optimize based on conversions or revenue.
Targeting RulesDate✓*✓**Supported but not recommended because push notifications are cached upon receipt and do not refresh.
Day of Week✓**Supported but not recommended because push notifications are cached upon receipt and do not refresh.
Query Parameters✓*✓***If using Airship with Salesforce Marketing Cloud, your identifiers in Airship (`channel_id` or `named_user`) must match Salesforce’s UUID and be passed through their connector.

**Can only use the UUID (mi_u) parameter.
Time of Day✓**Supported but not recommended because push notifications are cached upon receipt and do not refresh
Stories/Behavioral ActivityAirship’s channel or named user identifiers must be linked to your ESP’s UUID, which Movable Ink uses for Stories user profiles.
Deep linking in the app✓*✓***Must deep link with Branch.io.

**Must use Airship’s custom deep linking solution to track click analytics.
AppsBar Chart✓*As long as the UUID can be used in other data providers to generate content.
Barcode GeneratorRequires a custom solution and only UUIDs can be merged. Can be used with APIs that accept the UUIDs.
Countdown Timer✓*

*Supported but not recommended because push notifications are cached upon receipt and do not refresh.
Data Sources
Facebook Share ButtonRedirects to the web, then to Facebook’s website to post content. Deep linking to Facebook’s app is not possible.
Image PersonalizationAirship stores limited data about app activity, such as last login date, that may be used.
PollingAfter voting, will leave the app to a mobile landing page as long as the UUID is working.
QR Code GeneratorRequires a custom solution, and only UUIDs can be merged. Can be used with APIs that accept the UUIDs.
Scratch OffOn click, will either leave the app for the Scratch off experience or to a custom landing webpage built within the app.
Social Sharing✓**Will be directed to the original app, then the web browser, and finally to the desired destination.
VideoAnimated GIFs only.

Send a Push Notification with Movable Ink

You can insert an image source from a Movable Ink Creative Tag into an Airship message to deliver personalized media to your audience.

  1. Go to your campaign in Movable Ink and click Code.

  2. Click Push and copy the image source URL (the src of the img tag) from the creative tag.

  3. Go to the Content step of your message in Airship.

  4. Paste the URL in the Media section in the Content step of your message. Verify that the message preview shows your media from Movable Ink.

  5. Complete the remaining composer steps and send your message.

Send a Message Center message with Movable Ink

Airship Professional Services will create Message Center templates supporting Movable Ink for you. When you compose messages in Airship, you will select a template supporting Movable Ink, then add different parts of your Creative Tags from your Movable Ink campaign to your message.

In the Content step of a message in Airship:

  1. Choose how you want to present the message:

    • Add the message to the Message Center inbox only.
      1. Select Message Center and click Add Content.

      2. Select the Visual editor.

      OR
    • Add the message to the Message Center inbox AND link to the message from a push notification and/or in-app message.
      1. Select Message Center, combine with Push Notification and/or In-App Message, and click Add Content.

      2. Select the Visual editor in the Actions section. Message Center is automatically the selected action.

  2. Click Create.

  3. Select your Movable Ink template under Custom Templates.

  4. Go to the Content tab for the template.

  5. Enter your Movable Ink URL.

    1. Go to your campaign in Movable Ink, click Code, and select Mobile Inbox.
    2. Copy the URL in your Creative Tag. This is the value of the href attribute of the a tag.
    3. Click the Movable Ink URL field in Airship and paste the URL.
  6. Enter your Movable Ink Image.

    1. Go to your campaign in Movable Ink and copy the image source URL (the src of the img tag) from the creative tag.
    2. Click the Movable Ink Image field in Airship and paste the image source.
  7. Set up the remainder of your message.

  8. Click Save & Exit.

  9. Complete the remaining composer steps and send your message.

Set Up In-App Automation with Movable Ink

To take advantage of Movable Ink in your in-app automation, you must create a custom HTML message containing your Movable Ink Creative Tags.

  1. Go to your campaign in Movable Ink and click Code.

  2. Click In-App Messaging and copy the Creative Tags that you want to add to your automation.

  3. Paste your Creative Tags into the appropriate places in your Custom HTML message.

  4. In the Airship dashboard, click and select In App Automation.

  5. For Style, select Custom HTML.

  6. In the Content step, upload your Custom HTML file or provide the URL where your content is hosted.

  7. Complete the remaining composer steps and send your message.

Send an Email with Movable Ink

When setting up an email from Airship, you must add a Campaign Code to your email. If your email links your users to a website, you must add a Conversion Tracking script to your site to track conversions from your email.

To use Movable Ink Creative Tags in an email, you must copy the contents of your creative tags into your email. You can either copy your creative tags directly into your own, custom HTML email, or you can create HTML blocks in the email WYSIWYG editor.

In the Content step of your email:

  1. Go to your campaign in Movable Ink and click Code.

  2. Click Email and copy your Creative Tags.

  3. Paste your Creative Tags somewhere in the HTML Body of your email or create reusable HTML blocks in the WYSIWYG editor.

    Use your own custom HTML
    1. Paste your Creative Tags into your custom HTML.

    2. Click Add for HTML Body.

    3. Upload or paste HTML. Your HTML must contain the Campaign Code you copied in earlier steps.

    Create reusable custom HTML blocks in the WYSIWYG editor
    1. Select a default or saved layout, or select Blank Layout to design your own.

    2. Drag an HTML content element into your email.

    3. Select the HTML element and paste your Creative Tags into the block.

    4. When you finish setting up your email, click Done.

  4. Complete the remaining composer steps and send your message.

Deep Linking Using Movable Ink

Movable Ink deep links allow you to use dynamic in-app click through locations in your creative tags. They also enable you to track clicks within Movable Ink from your in-app campaigns. When using Movable Ink deep links you will need to do a few extra steps to make sure they integrate properly.

First, add a new Movable Ink deep link in the Airship dashboard. Set the template value to {url} to allow using any Movable Ink URL as a deep link.

Next, you will need to do a few minor app updates to handle Movable Ink deep links within a mobile app in order to resolve the actual deep link value at runtime.

Android Example

Set up a method or class that is able to resolve a Movable Ink deep link to the actual deep link:

class MovableInkUrl(private val url: String) {
    private data class Result(val url: String, val locationHeader: String?, val statusCode: Int)

    fun asFlow() = flow {
        emit(resolveDeepLink())
    }.flowOn(Dispatchers.IO)

    fun asLiveData(): LiveData<String?> {
        return asFlow()
            .catch {
                emit(null)
            }
            .asLiveData()
    }

    private fun resolveDeepLink(): String? {
        var result: Result = resolveUrl(url)

        while (true) {
            if (result.statusCode == 200) {
                return result.url
            }

            if (result.statusCode == 302) {
                val nextUrl = nextUrl(result.url, requireNotNull(result.locationHeader))
                if (shouldResolve(nextUrl)) {
                    result = resolveUrl(nextUrl)
                    continue
                } else {
                    return nextUrl
                }
            }

            return null
        }

        return null
    }

    private fun nextUrl(url: String, location: String): String {
        return if (Uri.parse(location).isRelative) {
            Uri.parse(url).buildUpon()
                .encodedPath(location)
                .build()
                .toString()
        } else {
            location
        }
    }

    private fun shouldResolve(url: String): Boolean {
        if (url == this.url) {
            return true
        }

        return URLUtil.isHttpsUrl(url)
    }

    private fun resolveUrl(url: String): Result {
        return with(URL(url).openConnection() as HttpURLConnection) {
            this.instanceFollowRedirects = false
            this.requestMethod = "HEAD"
            val l = getHeaderField("Location")
            disconnect()
            Result(url, l, responseCode)
        }
    }

}

Resolving deep links might take a few seconds depending on the connection, so you may need to show some sort of UI state while the app is determining the next navigation. In this example, we will show a progress activity that displays while the deep link is being resolved.

class MovableInkActivity : AppCompatActivity() {

    internal lateinit var viewModel: UrlViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Autopilot.automaticTakeOff(this)

        val url = intent.getStringExtra("url")
        val fallbackUrl = intent.getStringExtra("fallback")

        if (url == null) {
            finish()
            return
        }

        setContentView(createLayout())


        viewModel = ViewModelProvider(this, ViewModelFactory(url, fallbackUrl)).get(
            UrlViewModel::class.java
        )

        viewModel.result.observe(this) { result ->
            if (result != null) {
                UAirship.shared().deepLink(result)
            } else {
                // no deep link or fallback state
                // Toast.makeText(this, "Failed to resolve movable ink URL", Toast.LENGTH_SHORT).show()
            }

            finish()
        }
    }

    private fun createLayout(): View {
        val layout = FrameLayout(this)
        val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleLarge)
        progressBar.isIndeterminate = true

        val params: LinearLayout.LayoutParams =
            LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
        layout.addView(progressBar, params)

        return layout
    }

    companion object {
        fun launch(context: Context, url: String, fallbackUrl: String?) {
            val intent = Intent()
                .setClass(context, MovableInkActivity::class.java)
                .putExtra("url", url)
                .putExtra("fallback", fallbackUrl)
                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

            context.startActivity(intent)
        }
    }

    internal class UrlViewModel(private val url: String, private val fallbackUrl: String?) :
        ViewModel() {

        val result = MovableInkUrl(url).asFlow()
            .catch {
                emit(fallbackUrl)
            }
            .map {
                it ?: fallbackUrl
            }
            .asLiveData()
    }

    internal class ViewModelFactory(private val url: String, private val fallbackUrl: String?) :
        ViewModelProvider.Factory {

        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            @Suppress("UNCHECKED_CAST") return UrlViewModel(url, fallbackUrl) as T
        }
    }
}

To handle Movable Ink deep links through Airship you will need to either add a new deep link listener on Airship, or modify your existing deep link listener to process the deep links:

 airship.deepLinkListener = object : DeepLinkListener {
            override fun onDeepLink(deepLink: String): Boolean {
                if (deepLink.startsWith("https://mi.<MYDOMAIN>")) {
                    MovableInkActivity.launch(context, deepLink, null)
                    return true
                }

                ...

            }
        }

iOS Example

Set up a method or class that is able to resolve a Movable Ink deep link to the actual deep link:

}
import Foundation

class MovableInkURL {
    private let url: URL
    private let delegate: TaskDelegate = TaskDelegate()

    private lazy var session : URLSession = {
        let config = URLSessionConfiguration.default
        return URLSession(configuration: config,
                          delegate: self.delegate,
                          delegateQueue: OperationQueue())
    }()

    init(url: URL) {
        self.url = url
    }

    public func resolve() async throws -> URL {
        var request = URLRequest(url: url)
        request.httpMethod = "HEAD"

        return try await withCheckedThrowingContinuation { continuation in
            let task = self.session.dataTask(with: request, completionHandler: { data, response, error in

                if let response = response as? HTTPURLResponse,
                   let locationHeader = response.value(forHTTPHeaderField: "Location"),
                   let url = URL(string: locationHeader) {
                    return continuation.resume(returning: url)
                } else if let url = response?.url {
                    return continuation.resume(returning: url)
                } else {
                    return continuation.resume(throwing: error ?? URLError(.badServerResponse))
                }
            })

            task.resume()
        }
    }

    private class TaskDelegate: NSObject, URLSessionTaskDelegate {
        func urlSession(_ session: URLSession,
                        task: URLSessionTask,
                        willPerformHTTPRedirection response:
                        HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest? {

            guard let url = request.url,
                  url.absoluteString.lowercased().starts(with: "https")
            else {
                return nil
            }

            return request
        }
    }
}

Resolving deep links might take a few seconds depending on the connection, so you may need to show some sort of UI state while the app is determining the next navigation. In this example, we will show a progress window with a progress view while it resolves.

import Foundation
import SwiftUI
import UIKit

class MovableInkAlert {

    static func openForURL(_ url: URL,
                          scene: UIWindowScene,
                          onResult: @escaping (URL?) -> Void) {

        var window: UIWindow?
        let view = MovableLoadingView(url: url) { url in
            window?.windowLevel = .normal
            window?.isHidden = true
            window = nil
            onResult(url)
        }

        let viewController = UIHostingController(rootView: view)
        viewController.view.backgroundColor = .clear
        viewController.modalPresentationStyle = .currentContext
        window = UIWindow(windowScene: scene)
        window?.rootViewController = viewController
        window?.windowLevel = .alert
        window?.makeKeyAndVisible()
    }

    fileprivate struct MovableLoadingView: View {

        let url: URL
        let onResult: ((URL?) -> Void)

        @ViewBuilder
        var body: some View {
            VStack {
                ProgressView()
                    .onAppear {
                        Task {
                            let url = try? await MovableInkURL(url: self.url).resolve()
                            await MainActor.run {
                                self.onResult(url)
                            }
                        }
                    }
                    .padding()
                    .background(Color(UIColor.systemBackground))
                    .cornerRadius(10)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.gray.opacity(0.2))
        }
    }
}

To handle Movable Ink deep links through Airship you will need to either add a new deep link delegate on Airship, or modify your existing deep link delegate to process the deep links:

    func receivedDeepLink(_ url: URL, completionHandler: @escaping () -> ()) {
        if (url.absoluteString.starts(with: "https://mi.<MYDOMAIN>")) {
            MovableInkAlert.openForURL(url, scene: try! Utils.findWindowScene()) { resolvedURL in
                // Navigate to resolvedURL or a fallbackURL
                completionHandler()
            }
            return
        }

        ...
    }