Webview monetization

This page explain how to monetize a webview within a native mobile application. It does not involve the SDK APIs, only webviews in native application environment.

Table of contents

  1. Context
  2. Setup
    1. Pre-requisites
    2. App Configuration
      1. Android App
      2. iOS App
      3. smart.js Configuration

Context

For the sake of simplicity and consistency in their content management / user experience, some media publishers display a lot of content of their apps (such as news, articles) inside webviews, just like on mobile-web. The native app container mostly serve as a directory / “additional features” provider, but end users will consume mobile-web.

To monetize their mobile-web inventory, some publishers use smart.js and “mobile-web” formats, but the monetization is poor due to cookies restrictions in native apps webviews (not shared, not persistant).

To enhance the monetization, the publisher can requalify the inventory as “In-app” when passing the parameters AppName, BundleId and User IFA to the delivery engine, which will then forward those informations to the DSPs.

Setup

Pre-requisites

This setup makes sense only for publishers who:

  • monetize their mobile-web inventory through smart.js (Standard or OneCall).
  • display part of this inventory in a native application (iOS / Android).

App Configuration

Android App

The publisher should retrieve AppName, BundleId, IFA and GDPR Consent String and pass them to the WebView via a JavascriptInterface before the content of the webview is loaded.

It is up to the publisher to forward these information to the webview again if the consent string has changed.

Important note: this native implementation relies on the Android Webview API that allows native code to be called from javascript code running in the Webview. This is discouraged by Google if you do not fully control the html code that will be executed by the Webview (source), and might lead to Play Store rejection.

First, create a AsyncTask child class to handle the Javascript Interface.

private class AdvertisingIdAsyncTask(
  private val context: Context,
  private val appName: String,
  private val bundleId: String
  ) : AsyncTask<Void?, Void?, String?>() {

  private var advertisingId: String = ""

  init {
      // self execute task
      execute()
  }

  @Synchronized
  override fun doInBackground(vararg p0: Void?): String {
      // background code to fetch Advertising Id (must be run outside of main thread per google doc)
      var adInfo: AdvertisingIdClient.Info? = null
      try {
          adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context.applicationContext)
          advertisingId = adInfo.id ?: ""
      } catch (e: java.lang.Exception) {
          // ...
      }

      return advertisingId
  }

  @JavascriptInterface
  fun getAppName(): String {
      return appName
  }

  @JavascriptInterface
  fun getBundleId(): String {
      return bundleId
  }

  @JavascriptInterface
  fun getTcfString(): String {
      // In-app TCF v2 retrieved from the SharedPreferences (if set by an in-app CMP)
      val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
      val tcfString: String = sharedPreferences.getString("IABTCF_TCString", "") ?: ""
      return tcfString
  }

  @JavascriptInterface
  @Synchronized
  fun getAdvertisingId(): String {
      return advertisingId
  }
}

Then, before the webview load its content, retrieve all needed data and pass them to this new AsyncTask that will act as Javascript interface.

val packageManager: PackageManager = context.packageManager
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)

// Identification variables
val appName: String = packageManager.getApplicationLabel(applicationInfo) as String
val packageName: String = context.applicationContext.packageName

// AsyncTask that will act as Javascript interface
val asyncTask = AdvertisingIdAsyncTask(context, appName, packageName)

// Enable JavaScript then actually add the javascript interface
val settings = webview.settings
settings.javaScriptEnabled = true
webview.addJavascriptInterface(asyncTask, "SASIdProvider")

// Note that loading the request should happen after the injection of the variable since the
// Smartjs API require the variables in its setup method.
// Adding the user script after the page loading might prevent SmartJS to start and block ads completely.
webview.loadUrl("https://your-content-url.com")

iOS App

The publisher should first configure its webview in order to be able to display ads properly (including video ads), then retrieve the app name, the bundle ID, the advertising ID and the GDPR consent string and pass them to the webview before the content is loaded.

We recommend to use the IDFA with a fallback on the IFV as the advertising ID (because the IDFA requires user consent and the IFV is sufficient to ensure some features like frequency capping are working properly). We also recommend to inject the data using a WKUserScript before the actual content page is loaded!

It is up to the publisher to forward these information to the webview again if the consent string has changed.

// Webview configuration
webView.configuration.allowsInlineMediaPlayback = true
webView.configuration.websiteDataStore = WKWebsiteDataStore.default()

// Generic app info
let bundle = Bundle.main.bundleIdentifier!
let appName = Bundle.main.object(forInfoDictionaryKey: String(kCFBundleNameKey))!

// Advertising ID (IDFA by default with an IFV fallback to ensure that capping is working properly)
// Note that you must ask for user permission to retrieve the IDFA, check 'App Tracking Transparency'
// section of the iOS 'Getting Started' guide of the official documentation for more information
let advertisingID = ASIdentifierManager.shared().advertisingIdentifier.description != "00000000-0000-0000-0000-000000000000" ?
    ASIdentifierManager.shared().advertisingIdentifier.description :
    UIDevice.current.identifierForVendor?.description ?? ""

// GDPR consent string if available
let tcfString = UserDefaults.standard.object(forKey: "IABTCF_TCString") ?? ""

// Sending the data to the webview using an user script
let sasJavascript = """
    class SASIdProvider {
        static getAdvertisingId() { return '\(advertisingID)'; };\
        static getAppName() { return '\(appName)'; };\
        static getBundleId() { return '\(bundle)'; };\
        static getTcfString() { return '\(tcfString)'; };\
    };
"""
let sasScript = WKUserScript(source: sasJavascript, injectionTime: .atDocumentStart, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(sasScript)

// Note that the user script should be set on the webview BEFORE the page is loaded since the
// SmartJS API require the variables in its setup method.
// Adding the user script after the page loading might prevent SmartJS to start and block ads completely.
webView.load(URLRequest(url: URL(string: "https://domain/content.html")!))

smart.js Configuration

The publisher should setup smart.js this way:

<script type="application/javascript">
  sas.setup({ networkid: PUB_NETWORK_ID, domain: "PUB_WEBSITE_DOMAIN", uid: SASIdProvider.getAdvertisingId(), bundleId: SASIdProvider.getBundleId(), appName: SASIdProvider.getAppName() });
  sas.setGdprConsentData(SASIdProvider.getTcfString());
</script>

Note that SASIdProvider is the object passed by the native application to the webview.


Back to top

Copyright Equativ © 2024. All right reserved.

Page last modified: Dec 2 2024.