Making an In-App WebView message

Sample Formats

Introduced in Batch 1.17, In-App WebView messages enable you to display any web content in your app, while still leveraging Batch's campaign targeting, push and In-App trigger engine.

Any HTML content can be displayed. A handful of advanced fullscreen or modal scenarios are therefore possible:

  • Ultra-customized look and feel and layout
  • Multi-screens In-Apps
  • Customer surveys, quiz, NPS, forms (data collection)
  • Video players

Batch In-App WebView JavaScript SDK

Batch provides a JavaScript SDK that enables your message's JS code to communicate with the native SDKs.

It provides the following features:

  • Message dismissal
  • Opening deeplinks
  • Triggering Batch Actions (Android / iOS)
  • Getting installation data, such as the Installation ID
  • Getting campaign data, such as the Custom Payload or Tracking ID

The JavaScript SDK makes heavy uses of Promises to asynchronously provide results.

All available methods are documented on GitHub. TypeScript definitions are published on the npm package.

Script Integration

Batch's In-App WebView SDK exposes its API in a global object named window.batchInAppSDK.

Basic integration

Simply add the script to your page. As the script is very light, we encourage you to synchronously load it in so that it is always available:

<script src="https://cdn.jsdelivr.net/npm/@batch.com/in-app-webview-sdk@1.0/build/batch-webview-sdk.min.js"></script>

You are free to download and rehost this script.

Integration using NPM and a module bundler

The SDK can be added using a module bundler, such as webpack.

All you need to do is install the @batch.com/in-app-webview-sdk npm package, and import it.
As the SDK sets up a global variable, its import should not be used directly:

import _ from '@batch.com/in-app-webview-sdk'
console.log(window.batchInAppSDK.getPlatform())

TypeScript typings will automatically be available.

Using Batch Actions

Batch Actions (Android / iOS) both built-in and custom can be triggered using JavaScript.
Triggering an action will dismiss the message.

batchInAppSDK.performAction('action_name', {})
// An [analytics ID](#analyics) can be optionally provided
batchInAppSDK.performAction('action_name', {}, 'my_button_id')

// Example: track an event
const eventParameters = {
  e: 'survey_reply', // Event name
  l: '2021_opinion', // Event label
  a: {
    // Event attributes
    response: 'option_a',
  },
}
batchInAppSDK.performAction('batch.user.event', eventParameters)

The JS SDK can open deeplinks of any kind: both app-handled deeplinks and HTTP websites are supported.

Just like actions, opening a deeplink will dismiss the message.
You can control whether HTTP/HTTPS links should be opened in an in-app browser, or in the user's default browser app. If not specified, the campaign setting will be used.

As with any action related API, an analytics identifier can be provided.

// Opens batch.com
batchInAppSDK.openDeeplink('https://batch.com')

// Opens batch.com in an In-App browser, if possible
batchInAppSDK.openDeeplink('https://batch.com', true)

// Opens batch.com with a "my_button" analyticsId
// "undefined" as the second parameter tells Batch to use the default 'open in an in-app browser' campaign setting
batchInAppSDK.openDeeplink('https://batch.com', undefined, 'my_button')

// Opens an application deeplink
batchInAppSDK.openDeeplink('myapp://mycontent')

For more information about how deeplinking works, scroll down to Link Handling.
?batchAnalyticsID isn't supported on oppenDeeplink: use the analyticsId method parameter.

Note: batchInAppSDK.openDeeplink() is functionally equivalent to batchInAppSDK.performAction("batch.deeplink, ...), or <a href="..." target="_blank">.

Accessing installation data and identifiers

Installation ID

Batch's installation ID can be accessed from inside your message:

batchInAppSDK.getInstallationID().then(installationID => /* ... */)

Custom Language and Region

It is possible to read a language/region override that you have previously set on the native SDKs (Android / iOS).

You can access this in two ways:

  • Client-side, via the JavaScript SDK:
batchInAppSDK.getCustomRegion().then(/* ... */)
batchInAppSDK.getCustomLanguage().then(/* ... */)
  • Server-side, via HTTP headers:
X-Batch-Custom-Language: en
X-Batch-Custom-Region: US

Note: if the language/region has not been overriden, the header and JavaScript APIs will not reflect the default user language and region: the headers will be missing, and JS APIs will resolve with "undefined". Use standard HTTP headers and browser APIs to get the user's default language.

Advertising ID

If your application can use the Advertising ID, it can be retreived using JavaScript:

batchInAppSDK.getAdvertisingID()
  .then((advertisingID) => {
    /* Do something with it */
  })
  .catch(e => {
    /* Failed to get the advertising ID */
    console.error(e)
  };

When getting the Advertising ID fails for any reason, the SDK will throw an exception. Make sure you add a .catch() handler.

An unavailable Advertising ID can be because of any of the following:

  • You asked Batch to disable AdvertisingID/IDFA support when configuring the native SDK.
  • Your Android application does not have the required library to retrieve the Advertising ID
  • Your iOS application doesn't link with AdSupprot.framework or doesn't implement App Tracking Transparency.
  • Tracking is restricted by user preferences
  • The OS didn't provide any Advertising ID

Tracking ID

Tracking ID matches your campaign's Tracking ID field.
This is usually meant to be used with Event Dispatchers, but can be accessed in your message and used with your analytics solution.

batchInAppSDK.getTrackingID().then((trackingID) => {
  // trackingID = 2021_trip_discount
})

Custom payload

Custom payload can be read as a simple JS object. If the message is displayed in a Mobile Landing, you will get the complete custom payload attached to the push notification.

// Get a promocode from the custom payload
batchInAppSDK.getCustomPayload().then((payload) => {
  const promocode = payload['promocode']
})

Types

The custom payload value types are preserved in the following contexts:

  • iOS, both when opened via a Push Notification (Mobile Landing) and an In-App Campaign.
  • Android, when opened via an In-App Campaign.

Reading the custom payload while in a mobile landing on Android differs quite a bit: Due to a Firebase limitation, all values are of string type. Complex objects are stringified in the JSON notation, meaning that subtypes are preserved.

For example, the source custom payload (as set on the dashboard)

{
  "int_value": 1234,
  "complex_value": {
    "my_int": 4567
  }
}

becomes, when read in a WebView opened from a Mobile Landing:

{
  "int_value": "1234",
  "complex_value": "{\"my_int\": 4567}"
}

Make sure your custom payload handling code can detect and parse non-string values, or consider using string values on all platforms and mediums.

Example:

batchInAppSDK.getCustomPayload((payload) => {
  let complexValue = payload['complex_value']
  if (typeof complexValue === 'string') {
    // We're getting a serialized JSON as we're on an Android mobile landing
    complexValue = JSON.parse(complexValue)
  }
  const myInt = complexValue['my_int'] // 4567
})

Analytics

Analytic identifiers (not to be confused with the Tracking ID) can be attached to deeplinks, actions and dismissal via the JS SDK or batchAnalyticsID for links.

The analytic identifiers should:

  • Have limited cardinality. There is a per campaign limit on how many different analytic identifiers will be.
    A best practice is to use dedicated analytic identifiers that are not localized (therefore not using the button/link label).
  • Not be empty, or full of spaces.
  • Not be longer than 30 characters.

Invalid tracking identifiers will be ignored.

More info about the Analytics ID can be found on the dashboard documentation.

Standard HTML <a> links are supported:

  • Simple links will navigate inside of the format. Be careful if you're navigating to a website you do not control: Batch does not show any standard browser navigation control.
  • <a target="_blank"> will dismiss the message and open the link in an external browser, or an in application browser (SFSafariViewController or a Chrome Custom Tab) depending on your campaign settings. This is like calling batchInAppSDK.openDeeplink(url).

Some URLs will force a certain behavior:

  • Links that have a scheme that is not HTTP/HTTPS (such as itms://) will be handled as a deeplink, dismissing the message and being redirected to the host OS.
  • Links that can be handled by an application might dismiss the format and redirect the user to the app. Whether this happens or not depends on user settings.
  • App Store/iTunes Store links always dismiss the message and open the store.

Adding an analyticsId on standard links is also supported if the link is attributed with target="_blank": add a batchAnalyticsID query parameter to the URL. Example: https://batch.com/?batchAnalyticsID=my_click_id is the same as doing batchInAppSDK.openDeeplink('https://batch.com', undefined, 'my_click_id).

Special links and _blank navigation are ignored when opened inside an iframe. Only the main frame will get link handling: iframes can only navigate to a webpage.

Interception

Any link that is not opened inside of the WebView (that is, a link that dismisses the message before being opened) is handled by Batch's deeplink handling.

This means that like push deeplinks, or native In-App formats deeplinks, links will go through BatchDeeplinkDelegate on iOS and BatchDeeplinkInterceptor on Android.

Dismissal

Batch will always show a native close button on top of your web content. Android's back button is also supported as expected by users.

If you want to trigger a dismissal, you can use JavaScript:

  • batchInAppSDK.dismiss(). An Analytics ID can be set as the first parameter: batchInAppSDK.dismiss("ask_later").
  • window.close()

Optimizing content presentation

Viewport

When displaying web content, iOS and Android might adopt a desktop viewport. Text and UI Elements might become unreadable.

To control this, use a <meta name="viewport"> in <head>.

The most common viewport configuration to use for In-App WebViews should:

  • Set the content to use the device's native size
  • Disable user-controlled zoom
  • Display content at 100% scale

This can be achieved using the following:

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0" />

You can get more documentation about viewport configuration on MDN.

Note: iOS implements the 'viewport-fit' content attribute. See iOS Safari Specificities for more info.

Responsive design

Image showing multiple phones and tablets

Be aware that your message might be displayed in virtually any display size and aspect ratio:

  • iPhones (from an iPhone 5 to 12 Pro Max)
  • Android phones
  • Landscape phones
  • Tablets both portrait and landscape
  • Tablets in split screen mode (1/3 of the screen) or free floating windows

Use responsive web design techniques (CSS Media queries, Flexboxes, Grids, etc...) to handle this. Firefox, Chrome and Safari all come with tools that allow you to test your content on various form factors.

Light and Dark mode

WebViews support light and dark mode on both iOS and Android.

Use the prefers-color-scheme CSS media feature to change your style accordingly:

#content {
  background-color: lightgray;
  color: black;
}

@media (prefers-color-scheme: dark) {
  #content {
    background-color: black;
    color: white;
  }
}

Video

1.18.01.19.5

WebViews can be used to display video messages. To do so, you will need to wrap your video in HTML: directly linking a video as your WebView URL is not supported.

Videos will only autoplay if they have the autoplay and muted HTML attributes:

Example:

<html>
  <style>
    /* Put a HTML5 reset and a background color here */
  </style>
  <body>
    <video autoplay loop muted playsinline>
      <source src="https://my-cdn/video.webm" type="video/webm" />
      <source src="https://my-cdn/video.mp4" type="video/mp4" />
    </video>
  </body>
</html>

We encourage you to set multiple sources in different format and codecs. That way, you can serve smaller videos to supported browsers, while other can fall back on a more compatible format. See the <video> tag documentation for more info.

Note that in Low Power mode, videos will not autoplay on iOS. There is no way to change this behaviour: you will need to add a cover photo.

Low Power Mode

You should test your webpage in Low Power Mode (also often known as Battery Saver) on both iOS and Android as browsers might change behaviour in this mode:

  • Animations might skip frames or be skipped altogether
  • Videos might not be able to be autoplayed

Browser Specificities

iOS

Batch uses the system WKWebView on iOS devices.
WKWebView uses Safari's engine, which is updated with iOS but not via the App Store. This is quite different to what you may be used to on desktop, where major browsers like Firefox and Chrome are automatically updated.

Can I use... is a great ressource to see what a specific Safari version supports.

Safe Area

Modern iPhones and iPads have rounded screen corners, and sometimes a notch (that little black bar where the front cameras are). The screens end up having a non rectangular shape, and content might go under the rounding or the notch itself: users will not be able to see and interact with it.

Apple introduced a concept called "safe area": a rectangular shape where it is guaranteed that 100% of your content will be shown on screen. The default setting for web content is to limit it to the safe area. Borders will be shown around your content, that Batch will fill with your theme's static background color.

Whether your web content will expand into the "unsafe area" (which is the entire screen) is controlled by your <meta name="viewport"> tag.

By using viewport-fit=cover to your viewport, you tell iOS that you're aware of the screen cutouts, and will handle it.
If you do, you will need to add padding on your content using the env() CSS property. More info on the MDN. Failing to do so will make some content unreadable to the user.

This can be a tricky concept to understand, so here are some examples:
iOS Safe Area examples

  • The first screenshot has the following viewport: <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">.
    The "unsafe area" has been highlighted in purple for exaggeration. Note how web content is automatically put below it.

  • The second screenshot has this viewport: <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, viewport-fit=cover">, opting in to drawing in the unsafe area. It failed to take it into account using CSS: content ends up behind the system clock, indicators and notch.

  • The third screenshot uses the same viewport as the second, but has the following css rule on its content div:
    padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);.
    The web content expands in the unsafe area and draws its white background, hiding the theme's purple color.

Due to layouting bugs, avoid using 100vh or any vh related unit when using viewport-fit=cover. Use height: 100% instead.

Note: Do not hardcode margin/padding values mesured from env(). They are not the same for every device, especially when on an iPad.

Android

Batch uses the system WebView on Android devices.
The Android System WebView is based on Chromium and updated by Google via the Play Store: this means that even though your app may be running on a device running an old Android version, you can still use modern web technologies as the underlying Chrome version will be up to date.

As the rendering engine is Chrome, it behaves almost exactly like the Chrome application on an Android phone: if your message displays properly in it, it will most likely work as is in a Batch campaign.

Note: It is not possible to draw content behind the status bar and navigation bar using a HTML message.

Hosting

Requirements

Batch requires your In-App WebView messages to be served over HTTPS/HTTP like a standard webpage. There are no special requirements: if your hosting works in a browser, it will most likely work in an In-App message.

Be aware that Batch does not cache your message content: depending on your target audience, you might experience high peak loads.

A relatively cheap and reliable solution is to use a CDN (such as Cloudflare) or any service specialized in static content hosting (such as Netlify or Vercel). Free plans can sometimes be enough for your use cases, but make sure you check what the limits and the billing conditions are.

Response handling

Batch's WebViews will display any HTML page returned with a 2xx status code. Redirections are followed.

Custom 4xx/5xx error pages will not be displayed but will dismiss the format automatically.

Using plaintext HTTP

While we heavily discourage this practice, it is possible to use insecure HTTP URLs for your messages.
Android and iOS applications are by default configured to block plaintext networking. You will need to explicitly allow it:

Note: Changing those settings might make your entire app more vulnerable. Please consider using HTTPS if possible.

Previewing your message on the dashboard will not be possible.

Development

During development, you can use any static HTTP server to serve your pages to your test devices:

php -S 0.0.0.8081
# or
python3 -m http.server 8081
# or
npx http-server -p 8081

Trying out your message

In order to try your message, you first have to host it.
Once that is done, follow the steps described in How do In-App WebViews work? to create a theme and fill out your In-App campaign on the dashboard.

Set your URL and any other field you'd like to configure, then either send yourself a test push notification or save the campaign with a limited audience to get it on your test device.

Note that as In-App WebViews are webpages first and foremost, it is a good idea to test them first on iOS Safari and Android Chrome. Testing them only on your desktop browser is not fully accurate.

Debugging

iOS

Development mode

When In-App WebView messages are displayed in development mode. This mode is only (and automatically) enabled when using the test push feature of the dashboard.

Development mode:

  • Allows to reload the page by long pressing the close button
    iOS Reload Button

  • Explicitly shows why a message is dismissed because of an error (non 200 status code, SSL error, etc...)
    iOS Errors

Inspector

Web content opened on iOS can be debugged using Safari on macOS.

In order to show up in Safari's debug menu, your app needs to have been provisioned for development or ran in the iOS Simulator. Testflight or App Store apps can not be debugger.

First, you will need to enable Safari's develop menu. Open Safari, and open its preferences. Go to Advanced and check Show Develop menu in menu bar.

Safari Preferences

Then, open an In-App WebView message in your application (either on a device or on a simulator). Go back to safari, open the Develop menu. You should be able to find your device/simulator here.
Click on the menu item to open a remote inspector.

Safari develop menu

Once the remote inspector is open, you can use it as you would on desktop Safari.

Safari inspector

Android

Development mode

When In-App WebView messages are displayed in development mode. This mode is only (and automatically) enabled when using the test push feature of the dashboard.

Development mode:

  • Allows to reload the page by long pressing the close button
    Android Reload Button

  • Explicitly shows why a message is dismissed because of an error (non 200 status code, SSL error, etc...)
    Android Errors

Inspector

Web content opened on Android can be debugged using Chrome on Windows, Linux and macOS.

In order to show up in Chrome's debug menu, your app needs to enable WebView debugging explicitly and your device needs USB debugging to be enabled.

To enable WebView debugging, add in your Application subclass' onCreate():

WebView.setWebContentsDebuggingEnabled(true);

Note: This makes WebViews debuggable even if your application's debuggable manifest attribute is false. See Google's documentation for a way to only enlable this for debug builds, or remove this when compiling for production.

First, please follow Google Chrome's tutorial on how to enable remote debugging if you have not already.

Then, open an In-App WebView message in your application (either on a device or on a emulator). Go back to Chrome and open chrome://inspect using the adress bar. You should be able to find your device/emulator here.
Click on inspect to open a remote inspector.

Chrome inspect devices

Once the remote inspector is open, you can use it as you would on desktop Safari.

Chrome inspector

Samples

Sample HTML messages can be found on our public github repository.

They implement the best practices (mobile viewport, dark mode support), and illustrate the use of the In-App WebView Javascript SDK.