Syncing deviceUUID Between Android Mobile SDK and JS SDK in a WebView App

👍

What this step achieves

A single customer profile is created in Maestra, capturing events from both the mobile SDK and the JS SDK.

If your app uses WebView, it may also run the website's JS tracker. This means that when the mobile app is installed, Maestra will create two separate customer profiles for the same user — one from the JS SDK and one from the mobile SDK, each with a different deviceUUID.

To avoid duplicate profiles and keep all data in a single customer record, follow the steps below.


Step 1: Add a method to retrieve deviceUUID

companion object {
    // Set your preferred timeout for waiting on deviceUUID.
    // If deviceUUID is not received on first init,
    // sync will happen on the next page load / app launch.
    // If you're using third-party cookies, you must wait for deviceUUID —
    // otherwise sync will not occur.
    const val FETCHING_DEVICE_UUID_TIMEOUT = 5000L // in milliseconds
}

// Waits for deviceUUID for up to FETCHING_DEVICE_UUID_TIMEOUT ms
private suspend fun getDeviceUUID(): String = withTimeout(FETCHING_DEVICE_UUID_TIMEOUT) {
    suspendCancellableCoroutine { continuation ->
        Mindbox.subscribeDeviceUuid { uuid ->
            if (uuid.isNotEmpty()) {
                continuation.resume(uuid)
            } else {
                continuation.resumeWithException(Exception("Device UUID is empty"))
            }
        }
    }
}
📘

Timing expectations

On the first app launch, deviceUUID may take a few seconds to be returned by the provider.
On subsequent launches, it is typically available within 100–200 ms.

🚧

Third-party cookies

By default, third-party cookies are disabled starting from Android API 21. Make sure you are not calling CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true) unless your implementation requires it.

If you are using third-party cookies, you must wait for deviceUUID on the first initialization — otherwise synchronization will not happen.


Step 2: Wait for deviceUUID before loading the page

Add the following to your MainActivity.onCreate:

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

    // Initialize your WebView and other components

    // Launch a coroutine to wait for deviceUUID before loading the page
    CoroutineScope(Dispatchers.Main).launch {
        try {
            // Fetch deviceUUID without blocking the main thread
            deviceUUID = withContext(Dispatchers.IO) { getDeviceUUID() }
            Mindbox.writeLog("DeviceUUID for sync received: $deviceUUID", logLevel = Level.DEBUG)
            webView.loadUrl(URL)
        } catch (e: TimeoutCancellationException) {
            Mindbox.writeLog("Timeout waiting for deviceUUID. Loading without UUID", logLevel = Level.DEBUG)
            webView.loadUrl(URL)
        } catch (e: Exception) {
            Mindbox.writeLog("Failed to get deviceUUID: ${e.message}", logLevel = Level.ERROR)
            webView.loadUrl(URL)
        }
    }

    // Rest of your onCreate code
}

Step 3: Sharing deviceUUID Between Android Mobile SDK and JS SDK

This method passes deviceUUID to the web page via cookies and localStorage:

private fun syncMindboxDeviceUUIDs(uuid: String) {
    webView.evaluateJavascript(
        """
        document.cookie = "mindboxDeviceUUID=$uuid";
        window.localStorage.setItem('mindboxDeviceUUID', '$uuid');
        """
    ) {
        Mindbox.writeLog("deviceUUID synced: $uuid", logLevel = Level.DEBUG)
    }
}

Step 4: Call syncMindboxDeviceUUIDs in onPageStartedcallback

private val webViewClientInstance: WebViewClient by lazy {
    object : WebViewClient() {
        override fun onPageStarted(view: WebView, url: String?, favicon: android.graphics.Bitmap?) {
            super.onPageStarted(view, url, favicon)
            Log.d(Utils.TAG, "Page started loading: $url")

            // Sync deviceUUID
            deviceUUID?.let {
                syncMindboxDeviceUUIDs(it)
            } ?: run {
                Mindbox.subscribeDeviceUuid { uuid ->
                    if (uuid.isNotEmpty()) {
                        deviceUUID = uuid
                        syncMindboxDeviceUUIDs(uuid)
                    }
                }
            }
        }
    }
}

Full example

<details>
  <summary>MainActivity.kt — complete implementation</summary>

  ```kotlin
  package com.mindbox.example

  import android.content.Intent
  import android.os.Build
  import android.os.Bundle
  import android.util.Log
  import android.webkit.*
  import androidx.annotation.RequiresApi
  import androidx.appcompat.app.AppCompatActivity
  import cloud.mindbox.mobile_sdk.Mindbox
  import cloud.mindbox.mobile_sdk.logger.Level
  import com.mindbox.example.databinding.ActivityMainBinding
  import kotlinx.coroutines.*
  import kotlin.coroutines.*

  @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
  class MainActivity : AppCompatActivity() {

      companion object {
          const val URL = "https://your-website.com/"
          const val FETCHING_DEVICE_UUID_TIMEOUT = 4000L
      }

      private lateinit var webView: WebView
      private var deviceUUID: String? = null
      private var _binding: ActivityMainBinding? = null
      private val binding: ActivityMainBinding get() = _binding!!

      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          _binding = ActivityMainBinding.inflate(layoutInflater)
          setContentView(binding.root)

          // Enable WebView debugging
          WebView.setWebContentsDebuggingEnabled(true)

          // Initialize WebView after Mindbox.init if init is called in the activity
          webView = binding.webView.apply {
              settings.javaScriptEnabled = true
              settings.domStorageEnabled = true
              webViewClient = webViewClientInstance
          }

          /***
           * Start loading the page after obtaining the deviceUUID.
           * On the first app launch, obtaining the deviceUUID may take several seconds.
           * If you don't wait for the deviceUUID, synchronization will occur on the next page load.
           * The waiting time can be adjusted in the FETCHING_DEVICE_UUID_TIMEOUT constant.
           ***/
          CoroutineScope(Dispatchers.Main).launch {
              try {
                  deviceUUID = withContext(Dispatchers.IO) { getDeviceUUID() }
                  Mindbox.writeLog("DeviceUUID for synchronization received: $deviceUUID", logLevel = Level.DEBUG)
                  webView.loadUrl(URL)
              } catch (e: TimeoutCancellationException) {
                  Mindbox.writeLog("Timeout while waiting for Device UUID. Loading without UUID", logLevel = Level.DEBUG)
                  webView.loadUrl(URL)
              } catch (e: Exception) {
                  Mindbox.writeLog("Failed to get Device UUID for synchronization: ${e.message}", logLevel = Level.ERROR)
                  webView.loadUrl(URL)
              }
          }

          binding.viewCookiesButton.setOnClickListener { showCookies() }

          processMindboxIntent(intent = intent, context = this)?.let { (url, payload) ->
              Log.d(Utils.TAG, "Data from push: url: $url, payload: $payload")
          }
      }

      override fun onNewIntent(intent: Intent) {
          super.onNewIntent(intent)
          processMindboxIntent(intent = intent, context = this)?.let { (url, payload) ->
              Log.d(Utils.TAG, "Data from push: url: $url, payload: $payload")
          }
          Mindbox.onNewIntent(intent)
      }

      override fun onDestroy() {
          super.onDestroy()
          _binding = null
      }

      private val webViewClientInstance: WebViewClient by lazy {
          object : WebViewClient() {
              override fun onPageStarted(view: WebView, url: String?, favicon: android.graphics.Bitmap?) {
                  super.onPageStarted(view, url, favicon)
                  Log.d(Utils.TAG, "Page started loading: $url")
                  deviceUUID?.let {
                      syncMindboxDeviceUUIDs(it)
                  } ?: run {
                      Mindbox.subscribeDeviceUuid { uuid ->
                          if (uuid.isNotEmpty()) {
                              deviceUUID = uuid
                              syncMindboxDeviceUUIDs(uuid)
                          }
                      }
                  }
              }
          }
      }

      private suspend fun getDeviceUUID(): String = withTimeout(FETCHING_DEVICE_UUID_TIMEOUT) {
          suspendCancellableCoroutine { continuation ->
              Mindbox.subscribeDeviceUuid { uuid ->
                  if (uuid.isNotEmpty()) {
                      continuation.resume(uuid)
                  } else {
                      continuation.resumeWithException(Exception("Device UUID is empty"))
                  }
              }
          }
      }

      private fun syncMindboxDeviceUUIDs(uuid: String) {
          webView.evaluateJavascript(
              """
              document.cookie = "mindboxDeviceUUID=$uuid";
              window.localStorage.setItem('mindboxDeviceUUID', '$uuid');
              """
          ) {
              Mindbox.writeLog("Device UUID synchronized: $uuid", logLevel = Level.DEBUG)
          }
      }

      private fun showCookies() {
          val cookies = CookieManager.getInstance().getCookie(URL)
          Log.d(Utils.TAG, "Cookies: $cookies")
          Mindbox.subscribeDeviceUuid { uuid ->
              Log.d(Utils.TAG, "mobile sdk deviceUUID=$uuid")
          }
          webView.evaluateJavascript(
              "(function() {return window.localStorage.getItem('mindboxDeviceUUID')})()"
          ) { result ->
              Log.d(Utils.TAG, "js sdk deviceUUID: $result")
          }
      }

      @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
      private fun clearAllCookies() {
          WebStorage.getInstance().deleteAllData()
          val cookieManager = CookieManager.getInstance()
          cookieManager.removeAllCookies { success ->
              if (success) {
                  Log.d(Utils.TAG, "All cookies cleared")
              } else {
                  Log.e(Utils.TAG, "Failed to clear cookies")
              }
          }
      }
  }
  ```
</details>

Debugging

Use the method below to verify sync is working correctly. It logs three values that should all match: the mobile SDK deviceUUID, the value stored in cookies, and the value stored in localStorage.

Run this after the JS tracker has initialized:

private fun showCookies() {
    val cookies = CookieManager.getInstance().getCookie(URL)
    Log.d(Utils.TAG, "Cookies: $cookies")

    Mindbox.subscribeDeviceUuid { uuid ->
        Log.d(Utils.TAG, "mobile sdk deviceUUID=$uuid")
    }

    webView.evaluateJavascript(
        "(function() { return window.localStorage.getItem('mindboxDeviceUUID'); })();"
    ) { result ->
        Log.d(Utils.TAG, "js sdk deviceUUID: $result")
    }
}