Syncing deviceUUID Between Flutter Mobile and JS SDKs in app Using a WebView
Expected result of the “Syncing deviceUUID Between Flutter Mobile and JS SDKs in app Using a WebView” step:A single customer profile is created in Maestra, combining actions from both the mobile SDK and the website using the JS SDK.
If your app uses a WebView that loads a website with the JS tracker, installing the mobile app will result in two separate customer profiles in Maestra, each with a different deviceUUID: one from the JS SDK and one from the mobile SDK.
To prevent this and keep all data in a single customer profile, follow the steps below:
Add a method to retrieve the deviceUUID in your app.
// Timeout for retrieving the deviceUUID.
// The page won’t start loading until this timeout expires.
// On first initialization, retrieving the deviceUUID can take a few seconds,
//subsequent attempts usually take less than 250 ms.
static const fetchDeviceUuidTimeout = Duration(milliseconds: 4000);
// Retrieves the deviceUUID within the specified timeout.
// Note: fetchDeviceUuidTimeout must not be less than 250 ms,
// Otherwise, the deviceUUID may not be retrieved in time.
Future<String> _fetchDeviceUUIDWithTimeout() async {
final completer = Completer<String>();
if (fetchDeviceUuidTimeout.inMilliseconds < 250) {
throw ArgumentError("Timeout must be at least 250 milliseconds.");
}
final timer = Timer(fetchDeviceUuidTimeout, () {
if (!completer.isCompleted) {
completer.completeError(
TimeoutException("Timeout while fetching Device UUID."),
);
}
});
Mindbox.instance.getDeviceUUID((deviceUUID) {
if (!completer.isCompleted) {
if (deviceUUID.isNotEmpty) {
completer.complete(deviceUUID);
} else {
completer.completeError(Exception("DeviceUUID is empty"));
}
}
});
return completer.future.whenComplete(() => timer.cancel());
}
On first initialization of the Mindbox SDK, the deviceUUID may be returned by the provider after a few seconds.On subsequent initializations, the deviceUUID is retrieved within 100–200 ms.
Using Third-Party CookiesStarting with Android API 21 and iOS 12, third-party cookies are disabled by default.
When using third-party cookies, make sure to wait for the deviceUUID to be retrieved during the initial initialization.
In initState, add a method that waits for the deviceUUID retrieval and WebViewController initialization.
initState, add a method that waits for the deviceUUID retrieval and WebViewController initialization.@override
void initState() {
super.initState();
_initializeDeviceUUIDAndWebView();
// This call ensures that even if deviceUUID retrieval
// times out during the initial load,
// synchronization will still occur on subsequent page loads
// or app launches.
Mindbox.instance.getDeviceUUID((uuid) {
deviceUUID = uuid;
});
//The rest of your code
}
// Attempts to retrieve the deviceUUID within the specified timeout (fetchDeviceUuidTimeout).
// The page starts loading either after the UUID is retrieved or when the timeout expires.
// If the UUID is not retrieved, synchronization will occur on the next app launch or page load.
Future<void> _initializeDeviceUUIDAndWebView() async {
try {
final uuid = await _fetchDeviceUUIDWithTimeout();
deviceUUID = uuid;
print('DeviceUUID initialized: $deviceUUID');
} catch (e) {
print('Failed to initialize DeviceUUID: $e');
} finally {
_initializeWebViewController();
setState(() {
_isWebViewInitialized = true;
});
}
}If you don’t want to wait several seconds during the initial initialization, duplicate the deviceUUID retrieval in initState. In this case, synchronization will occur on the next page load.
initState. In this case, synchronization will occur on the next page load. // Adding this method ensures that even if deviceUUID retrieval
// times out during the initial page load,
// synchronization will occur on subsequent page loads
// or app launches.
Mindbox.instance.getDeviceUUID((uuid) {
deviceUUID = uuid;
});Add a WebViewController initialization method and a deviceUUID synchronization method.
WebViewController initialization method and a deviceUUID synchronization method.// Initializes the WebView. Synchronization is performed in the onPageStarted callback.
Future<void> _initializeWebViewController() async {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) async {
if (deviceUUID != null) {
await _waitForJavaScriptReady(_controller);
await _synchronizeDeviceUUID(_controller, deviceUUID!);
}
},
),
)
..loadRequest(Uri.parse(url));
}
// Syncs the deviceUUID with the JS SDK.
Future<void> _synchronizeDeviceUUID(
WebViewController controller,
String uuid,
) async {
await controller.runJavaScript('''
document.cookie = "mindboxDeviceUUID=$uuid";
window.localStorage.setItem('mindboxDeviceUUID', '$uuid');
''');
Mindbox.instance.writeNativeLog(
message: "Device UUID synchronized with deviceUUID: $uuid",
logLevel: LogLevel.info,
);
}Add a method that waits for the JS context to become available when the page starts loading.
On iOS, the JS context is not immediately available in the onPageStarted callback, and JavaScript cannot be executed until the context becomes available.
// Method for waiting for the JavaScript context to be ready
Future<void> _waitForJavaScriptReady(WebViewController controller) async {
const int maxRetries = 10;
int attempts = 0;
const Duration retryInterval = Duration(milliseconds: 10);
while (attempts < maxRetries) {
// Adds a short delay before the first check
// to give the JavaScript context time to initialize.
await Future.delayed(retryInterval);
try {
final isReady = await controller.runJavaScriptReturningResult('''
(function() {
return typeof document.cookie !== "undefined" &&
typeof localStorage !== "undefined";
})();
''');
if (isReady == true) {
print("JavaScript context is ready.");
return;
}
print("JavaScript context not ready, retrying... [$attempts]");
} catch (e) {
print("Error during JavaScript readiness check: $e");
}
await Future.delayed(retryInterval);
attempts++;
}
throw TimeoutException(
"JavaScript context not ready after $maxRetries retries.",
);
}Example
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mindbox/mindbox.dart';
import '../push_info_page/push_info_page.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const String url = "https://your-site.com/";
// Timeout for retrieving the deviceUUID.
// The page will not start loading until this timeout expires.
// During the initial initialization, retrieval may take several seconds,
// while subsequent attempts usually take less than 250 ms.
static const fetchDeviceUuidTimeout = Duration(milliseconds: 4000);
late final WebViewController _controller;
String? deviceUUID;
bool _isWebViewInitialized = false;
@override
void initState() {
super.initState();
_initializeDeviceUUIDAndWebView();
// This call ensures that even if deviceUUID retrieval
// times out during the first page load,
// synchronization will occur on subsequent page loads
// or app launches.
Mindbox.instance.getDeviceUUID((uuid) {
deviceUUID = uuid;
});
}
// Attempts to retrieve the device UUID within the specified timeout.
// The page starts loading either after the UUID is retrieved
// or when the timeout expires.
// If the UUID is not retrieved, synchronization will occur
// on the next app launch or page load.
Future<void> _initializeDeviceUUIDAndWebView() async {
try {
final uuid = await _fetchDeviceUUIDWithTimeout();
deviceUUID = uuid;
print('DeviceUUID initialized: $deviceUUID');
} catch (e) {
print('Failed to initialize DeviceUUID: $e');
} finally {
_initializeWebViewController();
setState(() {
_isWebViewInitialized = true;
});
}
}
// Initializes the WebView. Synchronization is performed in `onPageStarted`.
Future<void> _initializeWebViewController() async {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) async {
if (deviceUUID != null) {
await _waitForJavaScriptReady(_controller);
await _synchronizeDeviceUUID(_controller, deviceUUID!);
}
},
),
)
..loadRequest(Uri.parse(url));
}
// Retrieves the deviceUUID within the specified timeout.
Future<String> _fetchDeviceUUIDWithTimeout() async {
final completer = Completer<String>();
if (fetchDeviceUuidTimeout.inMilliseconds < 250) {
throw ArgumentError("Timeout must be at least 250 milliseconds.");
}
final timer = Timer(fetchDeviceUuidTimeout, () {
if (!completer.isCompleted) {
completer.completeError(
TimeoutException("Timeout while fetching Device UUID."),
);
}
});
Mindbox.instance.getDeviceUUID((deviceUUID) {
if (!completer.isCompleted) {
if (deviceUUID.isNotEmpty) {
completer.complete(deviceUUID);
} else {
completer.completeError(Exception("DeviceUUID is empty"));
}
}
});
return completer.future.whenComplete(() => timer.cancel());
}
void _handlePushNotification(String link, dynamic payload) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PushInfoPage(link: link, payload: payload),
),
);
}
// Debug method: logs cookies, localStorage, and the mobile UUID.
Future<void> _showData(WebViewController controller) async {
try {
final cookies =
await controller.runJavaScriptReturningResult("document.cookie");
print("Cookies: $cookies");
print("Mobile device UUID is: $deviceUUID");
final localStorageUUID = await controller.runJavaScriptReturningResult('''
window.localStorage.getItem('mindboxDeviceUUID');
''');
print("JS tracker device UUID: $localStorageUUID");
} catch (e) {
print("Failed to fetch cookies: $e");
}
}
// Syncs the deviceUUID with the JS SDK.
Future<void> _synchronizeDeviceUUID(
WebViewController controller,
String uuid,
) async {
await controller.runJavaScript('''
document.cookie = "mindboxDeviceUUID=$uuid";
window.localStorage.setItem('mindboxDeviceUUID', '$uuid');
''');
Mindbox.instance.writeNativeLog(
message: "Device UUID synchronized with deviceUUID: $uuid",
logLevel: LogLevel.info,
);
}
// Clears cookies.
Future<void> _clearAllCookies() async {
final cookieManager = WebViewCookieManager();
await cookieManager.clearCookies();
}
// Waits until the JavaScript context is available.
Future<void> _waitForJavaScriptReady(WebViewController controller) async {
const int maxRetries = 10;
int attempts = 0;
const Duration retryInterval = Duration(milliseconds: 10);
while (attempts < maxRetries) {
await Future.delayed(retryInterval);
try {
final isReady = await controller.runJavaScriptReturningResult('''
(function() {
return typeof document.cookie !== "undefined" &&
typeof localStorage !== "undefined";
})();
''');
if (isReady == true) {
print("JavaScript context is ready.");
return;
}
print("JavaScript context not ready, retrying... [$attempts]");
} catch (e) {
print("Error during JavaScript readiness check: $e");
}
await Future.delayed(retryInterval);
attempts++;
}
throw TimeoutException(
"JavaScript context not ready after $maxRetries retries.",
);
}
@override
Widget build(BuildContext context) {
if (!_isWebViewInitialized) {
return Scaffold(
appBar: AppBar(title: const Text('WebView Example')),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
],
),
),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('WebView Example'),
actions: [
IconButton(
icon: const Icon(Icons.cookie),
onPressed: () => _showData(_controller),
),
],
),
body: WebViewWidget(controller: _controller),
);
}
@override
void dispose() {
_controller.clearCache();
super.dispose();
}
}Updated 7 months ago

