Skip to content

Expo - React Native

Overview

Getting started for apps that are built with Expo utilizing Continuous Native Generation (CNG) and Prebuild.

If you're not using Expo, CNG and Prebuild, you should follow the React Native docs instead.

Not sure what CNG is? Read more about CNG here.

Installation

  1. Install the SDK

    $ npm install @movable/react-native-sdk
    
  2. iOS - Update your app.json to include the associatedDomains for your MovableInk domain. Also update the infoPlist to include the keys: movable_ink_universal_link_domains, movable_ink_api_key, and movable_ink_region.

    Key Description
    scheme If you don't already have a custom URI scheme for your app, add one here. This is required to support deeplinking - a scheme of myapp would allow links such as myapp://some/path to open your app.
    movable_ink_universal_link_domains The list of MovableInk domains that will open your app. This should match any MovableInk applinks in associatedDomains - Required for using Universal Links | App Links | Deeplinks
    movable_ink_api_key Your MovableInk SDK API Key - Required for using Behavior Events
    movable_ink_region Your MovableInk Region - Required for using Behavior Events
    {
      "expo": {
        "scheme": "myapp",
        "ios": {
          "associatedDomains": [
            "applinks:mi.domain.com"
          ],
          "infoPlist": {
            "movable_ink_universal_link_domains": [
              "mi.domain.com"
            ],
            "movable_ink_api_key": "API KEY",
            "movable_ink_region": "us" // or "eu"
          }
        }
      }
    }
    
  3. Android - Update your app.json to include the intentFilters for your MovableInk domain.

    {
      "expo": {
        "android": {
          "intentFilters": [
            {
              "action": "VIEW",
              "autoVerify": true,
              "data": [
                {
                  "scheme": "https",
                  "host": "mi.domain.com",
                  "pathPrefix": "/p/cpm"
                }
              ],
              "category": ["BROWSABLE", "DEFAULT"]
            },
            {
              "action": "VIEW",
              "data": [
                {
                  "scheme": "https",
                  "host": "mi.domain.com",
                  "pathPrefix": "/p/rpm"
                }
              ],
              "category": ["BROWSABLE", "DEFAULT"]
            },
            {
              "action": "VIEW",
              "data": [
                {
                  "scheme": "https",
                  "host": "mi.domain.com",
                  "pathPrefix": "/p/gom"
                }
              ],
              "category": ["BROWSABLE", "DEFAULT"]
            }
          ]
        }
      }
    }
    

    Make sure to update any instances of mi.domain.com to match your MovableInk domain.

    You'll also need to add a plugin to set your API Key:

    At the root of your project, create a folder called plugins if it doesn't already exist. In that plugins folder, create a file called mi_plugin.js

    .
    |- app/
    |  └─ plugins/
    |      └─ mi_plugin.js
    

    Add the following content:

    // mi_plugin.js
    const { withAndroidManifest, withAppBuildGradle } = require('@expo/config-plugins');
    
    module.exports = function movableInkPlugin(config, data) {
      // Handle AndroidManifest.xml
      config = withAndroidManifest(config, async (config) => {
        let androidManifest = config.modResults.manifest;
    
        // Handle application-level modifications
        if (androidManifest.application && androidManifest.application[0]) {
          const application = androidManifest.application[0];
    
          // Initialize meta-data array if it doesn't exist
          if (!application['meta-data']) {
            application['meta-data'] = [];
          }
    
          // Handle Movable Ink API key
          if (data.movable_ink_android_api_key) {
            application['meta-data'].push({
              $: {
                'android:name': 'com.movableink.inked.API_KEY',
                'android:value': data.movable_ink_android_api_key,
              },
            });
          }
        }
    
        return config;
      });
    
      // Handle build.gradle
      config = withAppBuildGradle(config, (config) => {
        const { modResults } = config;
    
        if (data.movable_ink_android_region) {
          const region = data.movable_ink_android_region.toUpperCase();
    
          // Add buildConfigField to defaultConfig
          const buildConfigField = `buildConfigField("String", "MOVABLE_INK_SDK_REGION", "\\"${region}\\"")`;
    
          // Find defaultConfig block and add the buildConfigField
          if (modResults.contents.includes('defaultConfig {')) {
            modResults.contents = modResults.contents.replace(
              /defaultConfig\s*\{/,
              `defaultConfig {\n        ${buildConfigField}`
            );
          }
        }
    
        return config;
      });
    
      return config;
    };
    

    Update your app.json to include the plugin:

    {
      "expo": {
        "plugins": [
          [
            "./plugins/mi_plugin",
            {
              "movable_ink_android_api_key": "API KEY",
              "movable_ink_android_region": "US" // or "EU"
            }
          ]
        ]
      }
    }
    
  4. Run prebuild

    $ npx expo prebuild
    
  5. Import RNMovableInk in your project and start the SDK as early as possible, such as in your app/_layout.tsx file:

    import RNMovableInk from '@movable/react-native-sdk';
    import { useEffect } from 'react';
    
    ...
    
    export default function RootLayout() {
      const colorScheme = useColorScheme();
    
      useEffect(() => {
        RNMovableInk.start(); 
      }, []);
    
      return (
        ...
      );
    }
    

Deeplinking

We highly recommend using Expo Routers Native Intents.

Create a special file called +native-intent.tsx at the top level of your project's app directory.

.
|- app/
|  └─ +native-intent.tsx

This file exports a special redirectSystemPath method designed to handle URL/path processing. When invoked, it receives an options object with two attributes: path and initial.

import RNMovableInk from '@movable/react-native-sdk';
import * as Linking from 'expo-linking';

export async function redirectSystemPath({ path, initial }) {
  try {
    console.log('Received Path:', path);

    let actualUrl = path;

    if (initial) {
      const initialUrl = await Linking.getInitialURL();
      console.log('Initial URL from Linking:', initialUrl);

      if (initialUrl) {
        actualUrl = initialUrl;
      }
    }

    // While the parameter is called `path` there is no guarantee that this is a path or a valid URL
    const url = new URL(actualUrl, 'myapp://');
    console.log('Received URL:', url.hostname);

    // Detection of third-party URLs will change based on the provider
    if (url.hostname === 'mi.domain.com') {
      // If this is a MovableInk URL, we need to initialize the SDK here before we can resolve it
      // If you already have `RNMovableInk.start()` in your app initialization code, that's ok as this is idempotent
      // It's important to call this before attempting to resolve any URLs
      RNMovableInk.start();

      const clickthroughLink = await RNMovableInk.resolveURL(url.toString());
      console.log('Resolved Clickthrough Link:', clickthroughLink);

      // Switch over the clickthrough link to determine where to navigate
      switch (clickthroughLink) {
        case 'https://example.com/explore':
          return '/(tabs)/explore';
        case 'https://example.com/special-offer':
          return '/(tabs)/special-offer';
        default:
          return '/(tabs)/index';
      }
    }

    return path;
  } catch {
    // Do not crash inside this function! Instead you should redirect users
    // to a custom route to handle unexpected errors.
    // Here we're just sending them to the home page. You should handle this error more gracefully in a production app.
    return '/(tabs)/index';
  }
}

If you need to store the clickthrough link instead for later use, you can do so by storing it in a global state manager such as Redux, Context API, or any other state management solution you are using.

Expo Linking

If you'd rather use Expo Linking directly, you can read how to set that up here.

Deferred Deeplinking

When a user attempts to open a MovableInk Link that is designated as a deferred deeplink on an iOS device, and they don't already have your app installed, MovableInk will enable Deferred Deeplinking. Instead of being directed to your website experience, they will be shown a page to open the App Store to download your app.

Once downloaded, MovableInk can check the pasteboard for the original link and allow you to open that location inside your app instead.

Before you can use Deferred Deeplinking, you'll need to setup the sdk_install event schema. You can read more about integrating Deferred Deeplinking here.

After you've setup Deferred Deeplinking, you'll need to enable the app install event before calling start:

// If using Deferred Deep Linking, make sure to enable the app install event before calling start
RNMovableInk.setAppInstallEventEnabled(true);
RNMovableInk.start();

// Call this anytime after you've called start when you're ready to check for a deferred deeplink
RNMovableInk.checkPasteboardOnInstall();

Warning

If this is ran on iOS 16+, this will prompt the user for permission to read from the pasteboard.

Behavior Events

Setting MIU (mi_u)

An MIU is an identifier used by your companies marketing team and email/mobile messaging provider, that when shared with MovableInk, allows to uniquely identify each of your customers when the interact with campaigns or any other MovableInk content.

Most distribution partners provide a unique user identifier as an available merge tag that distinctly identifies every recipient on your list(s). Using this merge tag allows MovableInk to provide users with a consistent experience when opening the same email multiple times and across multiple devices. Providing an MIU is required for accurate behavior and conversion tracking.

You'll need to work with your distribution partners to identify a unique user identifier. It must meet the following criteria:

You should make the following call as soon as the user is identified (usually after logging in) to set the MIU. Double check with your marketing team that the MIU you're using matches the values they are using. It's imperative that they match for accurate behavior event tracking.

RNMovableInk.setMIU("YOUR_MIU");

If your app has support for guest/anonymous users, you can still track events without setting an MIU. Once a user does login, you should call RNMovableInk.setMIU() with the MIU of the user, then call RNMovableInk.identifyUser(). This will attempt to associate any events that user made as a guest to this user.

Inherited MIUs

If a user interacts with a MovableInk Link that contains an MIU and deeplinks into your app, the SDK will use that MIU value automatically for all subsequent behavior events if you don't manually set an MIU.

If you manually set an MIU, events will use that instead.

Suggested MIU naming convention

We strongly recommend you use a UUID for MIUs.

If you find your ids include names, email addresses, timestamps, or are easily incremental (1, 2, 3), we suggest using a new naming method that is more secure so that your MIUs are not as easy to guess or impersonate.

Recommended NOT Recommended
42772706-c225-43ad-837e-c01e94907c3c user@example.com
d68d3dbe-86e1-43ce-bf5f-2dafd2f6af45 123456789
6ec4f1dd-0998-4ca8-8793-7511c2625a45 john-smith-123

Product Searched

Key Type Description Max
query String, required The query the user used for a given search 256
url String A URL for the given query 512
meta Record<String, unknown> An open Record of any extra data you'd like to associate to this event. The Value must be either a String, Boolean, or Numeric. Meta does not accept nested objects or arrays. 20 Items
RNMovableInk.productSearched({ query: 'Test Event' });

DaVinci Enabled - SDK v2

DaVinci will automatically receive the Product Searched event for DaVinci customers

Product Viewed

Key Type Description Max
id String, required The ID of a product 256
title String The title of the product 512
price String The price of the product in Dollars and Cents. Do not include currency or any other non-decimal digit characters.
currency Currency Enum The currency of the price. Defaults to USD. Introduced in v2. -
url String The URL of the product 512
categories Array<ProductCategory> An array of ProductCategory associated with the product. A ProductCategory must contain an id (String) and optionally a url (String) and/or title (String) 10 Items
meta Record<String, unknown> An open Record of any extra data you'd like to associate to this event. The Value must be either a String, Boolean, or Numeric. Meta does not accept nested objects or arrays. 20 Items
RNMovableInk.productViewed({
  id: "1",
  title: "Hyperion",
  price: "15.99",
  currency: Currency.USD,
  url: "https://inkrediblebooks.com/hyperion-dan-simmons",
  categories: [
    {
      id: "Sci-Fi",
      url: "https://inkrediblebooks.com/categories/scifi"
    },
    {
      id: "Fiction",
      url: "https://inkrediblebooks.com/categories/fiction"
    }
  ],
  meta: { pages: 496 }
});

DaVinci Enabled - SDK v2

DaVinci will automatically receive the Product Viewed event for DaVinci customers

v2

The currency key is introduced in v2. If you're using a version < v2, simply remove currency.

Product Added

Key Type Description Max
id String, required The ID of a product 256
title String The title of the product 512
price String The price of the product in Dollars and Cents. Do not include currency or any other non-decimal digit characters.
currency Currency Enum The currency of the price. Defaults to USD. Introduced in v2. -
url String The URL of the product 512
categories Array<ProductCategory> An array of ProductCategory associated with the product. A ProductCategory must contain an id (String) and optionally a url (String) and/or title (String) 10 Items
meta Record<String, unknown> An open Record of any extra data you'd like to associate to this event. The Value must be either a String, Boolean, or Numeric. Meta does not accept nested objects or arrays. 20 Items
void productAdded() {
RNMovableInk.productAdded({
  id: "1",
  title: "Hyperion",
  price: "15.99",
  url: "https://inkrediblebooks.com/hyperion-dan-simmons",
  categories: [
    {
      id: "Sci-Fi",
      url: "https://inkrediblebooks.com/categories/scifi"
    },
    {
      id: "Fiction",
      url: "https://inkrediblebooks.com/categories/fiction"
    }
  ],
  meta: { pages: 496 }
});
}

v2

The currency key is introduced in v2. If you're using a version < v2, simply remove currency.

Product Removed

Key Type Description Max
id String, required The ID of a product 256
title String The title of the product 512
price String The price of the product in Dollars and Cents. Do not include currency or any other non-decimal digit characters.
currency Currency Enum The currency of the price. Defaults to USD. Introduced in v2. -
url String The URL of the product 512
categories Array<ProductCategory> An array of ProductCategory associated with the product. A ProductCategory must contain an id (String) and optionally a url (String) and/or title (String) 10 Items
meta Record<String, unknown> An open Record of any extra data you'd like to associate to this event. The Value must be either a String, Boolean, or Numeric. Meta does not accept nested objects or arrays. 20 Items
void productRemoved() {
RNMovableInk.productRemoved({
  id: "1",
  title: "Hyperion",
  price: "15.99",
  currency: Currency.USD,
  url: "https://inkrediblebooks.com/hyperion-dan-simmons",
  categories: [
    {
      id: "Sci-Fi",
      url: "https://inkrediblebooks.com/categories/scifi"
    },
    {
      id: "Fiction",
      url: "https://inkrediblebooks.com/categories/fiction"
    }
  ],
  meta: { pages: 496 }
});
}

v2

The currency key is introduced in v2. If you're using a version < v2, simply remove currency.

Category Viewed

Key Type Description Max
id String, required The ID of a category 256
title String The title of the category 512
url String The URL of the category 512
meta Record<String, unknown> An open Record of any extra data you'd like to associate to this event. The Value must be either a String, Boolean, or Numeric. Meta does not accept nested objects or arrays. 20 Items
RNMovableInk.categoryViewed({
  id: "scifi",
  title: "Sci-Fi",
  url: "https://inkrediblebooks.com/categories/scifi",
  meta: { pages: 496 }
});

Order Completed

Key Type Description Max
id String The ID of a order 256
revenue String The total of the order in Dollars and Cents. Do not include currency or any other non-decimal digit characters.
currency Currency Enum The currency of the price. Defaults to USD. Introduced in v2. -
products Array<OrderCompletedProduct> An array of the products in an order. OrderCompletedProduct must contain an id (String), and optionally a price (String), quantity (Number), title (String), url (String), and meta (Record<String, unknown>). 100 Items
RNMovableInk.orderCompleted({
  id: "1",
  revenue: "15.99",
  currency: Currency.USD,
  products: [
    {
      id: "1",
      price: "15.99", 
      quantity: 1,
      title: "Hyperion",
      url: "https://inkrediblebooks.com/hyperion-dan-simmons",
      meta: { pages: 496 }
    }
  ]
});

DaVinci Enabled - SDK v2

DaVinci will automatically receive the Order Completed event for DaVinci customers.

Note that the other Order events that DaVinci supports are NOT supported by the SDK such as Order Refund / Cancellation.

v2

The currency key is introduced in v2. If you're using a version < v2, simply remove currency.

Custom Defined Events

In addition to the pre-defined events we provide, you can log custom events tailored to your business specific requirements. This flexibility enables you to monitor and understand user interactions beyond what our pre-defined events offer.

This type of event has a custom name name, such as video_started, and optional parameters as the payload.

The name and properties of a custom event must first be defined on your companies account. Please reach out to your Movable Ink client experience team at MovableInk to get started with this.

RNMovableInk.logEvent("video_started", {title: "Cat Bloopers"})

Testing Behavior Events

When you're ready to test an event, you need to set the MIU to a known value. You should let your Movable Ink client experience team know that you're ready to start testing events and give them the MIU that you are going to use before you do so. This will allow them to verify that the events are being sent correctly.

We usually use an empty UUID for testing, such as 00000000-0000-0000-0000-000000000000.

When you're ready to test an event, you should open the Console app on your Mac, select your device on the sidebar, then press the start button on the top bar. To filter the logs to just view MovableInk SDK logs, search for MI SDK.

When you send an event, you should see the event logged in the console. If the event was structured correctly, you should see a success message in the console along side the event name.

In App Message

The MovableInk SDK supports showing HTML based MI Content as In App Messages.

RNMovableInk.showInAppMessage("https://mi.example.com/p/rp/abcd12345.html", (buttonId) => {
  // User interacted with a link that has a buttonID
})

Note the link ends with .html. When exporting your links from Studio, make sure to add this suffix when using it as an In App Message in the SDK.

If you're already using an In App Message provider, such as Braze or Salesforce Marketing Cloud, you can still use MovableInk driven messages from those tools.

Go to the In App Message article to learn more.

SDK Size

The MovableInk SDK adds approximately 0.5 MB to the compressed app size and 1.8 MB to the uncompressed app size. The "compressed app size" refers to the size of the app when it is packaged and compressed for distribution, such as when it is downloaded. Once downloaded, the app is decompressed and installed on the user's device. The uncompressed size is what actually occupies space on the device.