Skip to content

Commit 3c51ca8

Browse files
feat: Add WebView integration tab for JS SDK
1 parent a3275e2 commit 3c51ca8

File tree

7 files changed

+427
-0
lines changed

7 files changed

+427
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>mParticle WebView Bridge Test</title>
7+
<style>
8+
body { font-family: sans-serif; background: #111; color: #fff; padding: 16px; }
9+
button { padding: 12px 14px; margin-right: 10px; border-radius: 10px; border: 0; background: #4079fe; color: #fff; font-weight: 600; }
10+
.muted { opacity: 0.7; }
11+
</style>
12+
<script type="text/javascript">
13+
// WebView-only snippet (NO API KEY) + required bridge name.
14+
const mySdkConfig = {
15+
requiredWebviewBridgeName: "higgsWebviewBridge",
16+
isDevelopmentMode: true
17+
};
18+
window.mParticle = { config: mySdkConfig };
19+
20+
(function () {
21+
window.mParticle = window.mParticle || {
22+
EventType: { Unknown: 0, Navigation: 1, Location: 2, Search: 3, Transaction: 4, UserContent: 5, UserPreference: 6, Social: 7, Other: 8 }
23+
};
24+
window.mParticle.eCommerce = { Cart: {} };
25+
window.mParticle.Identity = {};
26+
window.mParticle.config = window.mParticle.config || {};
27+
window.mParticle.config.rq = [];
28+
window.mParticle.ready = function (t) { window.mParticle.config.rq.push(t); };
29+
function e(e, o) {
30+
return function () {
31+
if (o) { e = o + "." + e; }
32+
var t = Array.prototype.slice.call(arguments);
33+
t.unshift(e);
34+
window.mParticle.config.rq.push(t);
35+
};
36+
}
37+
var o = ["endSession", "logError", "logEvent", "logForm", "logLink", "logPageView", "setSessionAttribute", "setAppName", "setAppVersion", "setOptOut", "setPosition", "startNewSession", "startTrackingLocation", "stopTrackingLocation"];
38+
var n = ["setCurrencyCode", "logCheckout"];
39+
var i = ["identify", "login", "logout", "modify"];
40+
o.forEach(function (t) { window.mParticle[t] = e(t); });
41+
n.forEach(function (t) { window.mParticle.eCommerce[t] = e(t, "eCommerce"); });
42+
i.forEach(function (t) { window.mParticle.Identity[t] = e(t, "Identity"); });
43+
var r = document.createElement("script");
44+
r.type = "text/javascript";
45+
r.async = true;
46+
r.src = "https://jssdkcdns.mparticle.com/js/v2/mparticle.js";
47+
var c = document.getElementsByTagName("script")[0];
48+
c.parentNode.insertBefore(r, c);
49+
})();
50+
</script>
51+
</head>
52+
<body>
53+
<h2>mParticle WebView Bridge</h2>
54+
<p class="muted">
55+
This page is loaded from <code>file:///android_asset</code> and uses the Web SDK in bridged mode.
56+
Click the button to log a JS event that should be forwarded through the Android SDK.
57+
</p>
58+
59+
<button id="btnEvent">Log JS Event</button>
60+
<button id="btnPage">Log Page View</button>
61+
62+
<hr style="border:0;border-top:1px solid rgba(255,255,255,0.15);margin:18px 0;" />
63+
64+
<h3 style="margin:0 0 10px 0;">Identity Alias (JS → Native)</h3>
65+
<p class="muted" style="margin-top:0;">
66+
This calls <code>mParticle.Identity.aliasUsers</code>. In WebView bridge mode this is queued as a JS request and
67+
forwarded to the Android SDK. We auto-populate a valid aliasRequest from the native SDK (MPIDs + time window),
68+
similar to the docs reference.
69+
</p>
70+
71+
<div style="display:flex;flex-direction:column;gap:10px;max-width:520px;">
72+
<div class="muted" id="aliasInfo">Waiting for native MPIDs…</div>
73+
<div>
74+
<button id="btnAlias" disabled style="opacity:0.6;">Send Alias Users</button>
75+
</div>
76+
</div>
77+
78+
<script>
79+
function log(msg) {
80+
// Keep logs out of the UI; use console instead.
81+
console.log((new Date().toISOString()) + " " + msg);
82+
}
83+
84+
document.getElementById("btnEvent").addEventListener("click", function () {
85+
if (!window.mParticle) return log("mParticle not available");
86+
window.mParticle.logEvent(
87+
"WebView JS Button Click",
88+
window.mParticle.EventType.Other,
89+
{ source: "webview", bridgeName: "higgsWebviewBridge" }
90+
);
91+
log("Called mParticle.logEvent('WebView JS Button Click')");
92+
});
93+
94+
document.getElementById("btnPage").addEventListener("click", function () {
95+
if (!window.mParticle) return log("mParticle not available");
96+
window.mParticle.logPageView("WebView Bridge Test", { source: "webview" });
97+
log("Called mParticle.logPageView('WebView Bridge Test')");
98+
});
99+
100+
window.__mp = window.__mp || {};
101+
window.__mp.aliasRequest = window.__mp.aliasRequest || null;
102+
103+
// Called by native via evaluateJavascript after page load
104+
window.setNativeAliasRequest = function (aliasRequest) {
105+
window.__mp.aliasRequest = aliasRequest || null;
106+
const info = document.getElementById("aliasInfo");
107+
const btn = document.getElementById("btnAlias");
108+
if (!window.__mp.aliasRequest) {
109+
info.textContent = "Missing aliasRequest from native SDK";
110+
btn.disabled = true;
111+
btn.style.opacity = "0.6";
112+
return;
113+
}
114+
const ar = window.__mp.aliasRequest;
115+
const valid = !!ar.sourceMpid && !!ar.destinationMpid && String(ar.sourceMpid) !== String(ar.destinationMpid);
116+
info.textContent =
117+
`Using sourceMpid=${ar.sourceMpid} → destinationMpid=${ar.destinationMpid} ` +
118+
`(scope=${ar.scope || "device"})`;
119+
btn.disabled = !valid;
120+
btn.style.opacity = valid ? "1" : "0.6";
121+
if (!valid) {
122+
info.textContent = "Invalid MPIDs from native SDK (need 2 unique users)";
123+
}
124+
};
125+
126+
// If native injected a pending request before this function was defined, apply it now.
127+
if (window.__mp && window.__mp._pendingAliasRequest) {
128+
window.setNativeAliasRequest(window.__mp._pendingAliasRequest);
129+
}
130+
131+
// Log screen view from JS (not native) when page loads
132+
window.mParticle.ready(function() {
133+
if (window.mParticle && typeof window.mParticle.logScreen === "function") {
134+
window.mParticle.logScreen("WebView Bridge");
135+
log("Called mParticle.logScreen('WebView Bridge') from JS");
136+
}
137+
});
138+
139+
document.getElementById("btnAlias").addEventListener("click", function () {
140+
// Send alias request from WebView (JS) per Web SDK docs:
141+
// https://docs.mparticle.com/developers/client-sdks/web/idsync/#user-aliasing
142+
if (!window.mParticle || !window.mParticle.Identity) return log("mParticle.Identity not available");
143+
if (typeof window.mParticle.Identity.aliasUsers !== "function") return log("mParticle.Identity.aliasUsers not available yet");
144+
if (!window.__mp.aliasRequest) return log("Alias blocked: missing aliasRequest (native injection not ready)");
145+
146+
const sourceMpidStr = String(window.__mp.aliasRequest.sourceMpid || "").trim();
147+
const destinationMpidStr = String(window.__mp.aliasRequest.destinationMpid || "").trim();
148+
149+
if (!sourceMpidStr || !destinationMpidStr) {
150+
return log("Alias blocked: missing MPIDs");
151+
}
152+
if (sourceMpidStr === destinationMpidStr) {
153+
return log("Alias blocked: sourceMpid and destinationMpid must be unique");
154+
}
155+
156+
// Parse MPIDs as numbers for the bridge (may lose precision for very large MPIDs,
157+
// but the bridge might require numeric values to convert to native long).
158+
const sourceMpidNum = Number(sourceMpidStr);
159+
const destinationMpidNum = Number(destinationMpidStr);
160+
161+
if (!Number.isFinite(sourceMpidNum) || !Number.isFinite(destinationMpidNum)) {
162+
return log(`Alias blocked: MPIDs not numeric (source=${sourceMpidStr}, dest=${destinationMpidStr})`);
163+
}
164+
if (sourceMpidNum <= 0 || destinationMpidNum <= 0) {
165+
return log(`Alias blocked: MPIDs must be positive (source=${sourceMpidNum}, dest=${destinationMpidNum})`);
166+
}
167+
168+
const now = Date.now();
169+
// Per Web SDK docs format: MPIDs as numbers, times as numbers (ms since epoch)
170+
const aliasRequest = {
171+
sourceMpid: sourceMpidNum,
172+
destinationMpid: destinationMpidNum,
173+
startTime: now - (30 * 60 * 1000), // last 30 minutes
174+
endTime: now,
175+
scope: "device"
176+
};
177+
178+
log(`Sending aliasRequest: sourceMpid=${sourceMpidNum} (from "${sourceMpidStr}"), destinationMpid=${destinationMpidNum} (from "${destinationMpidStr}")`);
179+
180+
window.mParticle.Identity.aliasUsers(aliasRequest, function (code, body) {
181+
log(`aliasUsers callback: code=${code} body=${JSON.stringify(body)}`);
182+
});
183+
log(`Called mParticle.Identity.aliasUsers(${JSON.stringify(aliasRequest)})`);
184+
});
185+
186+
log("Loaded. Waiting for user action…");
187+
</script>
188+
</body>
189+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.mparticle.example.higgsshopsampleapp.fragments
2+
3+
import android.os.Bundle
4+
import android.util.Log
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import android.view.ViewGroup
8+
import android.webkit.WebChromeClient
9+
import android.webkit.WebView
10+
import android.webkit.WebViewClient
11+
import androidx.fragment.app.Fragment
12+
import com.mparticle.MParticle
13+
import com.mparticle.identity.AliasRequest
14+
import com.mparticle.example.higgsshopsampleapp.R
15+
import com.mparticle.example.higgsshopsampleapp.activities.MainActivity
16+
import com.mparticle.example.higgsshopsampleapp.databinding.FragmentWebviewBridgeBinding
17+
18+
class WebViewBridgeFragment : Fragment() {
19+
20+
private lateinit var binding: FragmentWebviewBridgeBinding
21+
22+
private val tag = "WebViewBridgeFragment"
23+
private val bridgeName = "higgsWebviewBridge"
24+
private val assetUrl = "file:///android_asset/webview_bridge_test.html"
25+
26+
override fun onCreateView(
27+
inflater: LayoutInflater,
28+
container: ViewGroup?,
29+
savedInstanceState: Bundle?
30+
): View {
31+
(activity as MainActivity).setActionBarTitle("")
32+
inflater.context.setTheme(R.style.Theme_mParticle_SampleApp)
33+
binding = FragmentWebviewBridgeBinding.inflate(inflater, container, false)
34+
return binding.root
35+
}
36+
37+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
38+
super.onViewCreated(view, savedInstanceState)
39+
// All events should come from JS, not native - screen view will be logged from WebView
40+
41+
binding.btnAliasNative.setOnClickListener {
42+
aliasUsersNative()
43+
}
44+
45+
configureWebView(binding.webview)
46+
// Per docs, the WebView must be registered before loading content.
47+
MParticle.getInstance()?.registerWebView(binding.webview, bridgeName)
48+
binding.webview.loadUrl(assetUrl)
49+
}
50+
51+
private fun configureWebView(webView: WebView) {
52+
webView.settings.javaScriptEnabled = true
53+
webView.settings.domStorageEnabled = true
54+
webView.settings.allowFileAccess = true
55+
webView.settings.allowContentAccess = true
56+
57+
webView.webChromeClient = WebChromeClient()
58+
webView.webViewClient = object : WebViewClient() {
59+
override fun onPageFinished(view: WebView, url: String) {
60+
super.onPageFinished(view, url)
61+
injectAliasRequestIntoPage(view)
62+
}
63+
}
64+
}
65+
66+
/**
67+
* Inject MPIDs into the page so the WebView can send alias requests via mParticle.Identity.aliasUsers()
68+
*/
69+
private fun injectAliasRequestIntoPage(webView: WebView) {
70+
val identity = MParticle.getInstance()?.Identity() ?: return
71+
val users = try {
72+
identity.users
73+
} catch (_: Throwable) {
74+
identity.getUsers()
75+
}
76+
if (users.size < 2) return
77+
78+
// Match docs: users are in reverse chronological order:
79+
// source = users[1], destination = users[0]
80+
val sourceUser = users[1]
81+
val destinationUser = users[0]
82+
83+
val sourceMpid = try {
84+
sourceUser.id
85+
} catch (_: Throwable) {
86+
sourceUser.getId()
87+
}.toString()
88+
89+
val destinationMpid = try {
90+
destinationUser.id
91+
} catch (_: Throwable) {
92+
destinationUser.getId()
93+
}.toString()
94+
95+
Log.i(tag, "Mansi sourceMpid=$sourceMpid destinationMpid=$destinationMpid")
96+
97+
if (sourceMpid.isBlank() || destinationMpid.isBlank()) return
98+
if (sourceMpid == destinationMpid) return
99+
100+
val now = System.currentTimeMillis()
101+
val startTime = now - (30 * 60 * 1000)
102+
val endTime = now
103+
104+
val js =
105+
"window.__mp = window.__mp || {};" +
106+
"window.__mp._pendingAliasRequest = {sourceMpid:'$sourceMpid',destinationMpid:'$destinationMpid',startTime:$startTime,endTime:$endTime,scope:'device'};" +
107+
"if (window.setNativeAliasRequest) { window.setNativeAliasRequest(window.__mp._pendingAliasRequest); }"
108+
webView.evaluateJavascript(js, null)
109+
}
110+
111+
/**
112+
* Native alias invoked from the fragment button (and from the WebView bridge).
113+
* Uses the "working" approach: source=users[1], destination=users[0], time window = last 30 minutes.
114+
*/
115+
116+
// Kept native button as-is (optional): it calls the Android SDK alias directly.
117+
private fun aliasUsersNative() {
118+
val identity = MParticle.getInstance()?.Identity() ?: return
119+
val users = try {
120+
identity.users
121+
} catch (_: Throwable) {
122+
identity.getUsers()
123+
}
124+
if (users.size < 2) {
125+
binding.subtitle.text = getString(R.string.webview_bridge_alias_need_two_users)
126+
return
127+
}
128+
val sourceUser = users[1]
129+
val destinationUser = users[0]
130+
val now = System.currentTimeMillis()
131+
val request: AliasRequest = AliasRequest.Builder()
132+
.sourceMpid(sourceUser.id.toString().toLongOrNull() ?: 0L)
133+
.destinationMpid(destinationUser.id.toString().toLongOrNull() ?: 0L)
134+
.startTime(now - (30 * 60 * 1000))
135+
.endTime(now)
136+
.build()
137+
identity.aliasUsers(request)
138+
}
139+
140+
override fun onDestroyView() {
141+
// Avoid leaking the WebView.
142+
binding.webview.apply {
143+
stopLoading()
144+
loadUrl("about:blank")
145+
clearHistory()
146+
removeAllViews()
147+
destroy()
148+
}
149+
super.onDestroyView()
150+
}
151+
}
152+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:tint="#FFFFFF"
5+
android:viewportWidth="24"
6+
android:viewportHeight="24">
7+
<!-- Material "public" (globe) -->
8+
<path
9+
android:fillColor="@android:color/white"
10+
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-0.69 0.1,-1.35 0.29,-1.98L8,13.73V14c0,1.1 0.9,2 2,2v4.07C6.06,19.56 4,16.07 4,12zM18.93,17.36c-0.26,-0.81 -1,-1.36 -1.93,-1.36h-1v-3c0,-0.55 -0.45,-1 -1,-1H8v-2h2c0.55,0 1,-0.45 1,-1V6h2c1.1,0 2,-0.9 2,-2v-0.41C18.93,4.78 20,6.33 20,8c0,1.1 -0.9,2 -2,2h-1c-0.55,0 -1,0.45 -1,1v1c0,0.55 0.45,1 1,1h3.63c-0.55,1.79 -1.66,3.33 -3.08,4.36z" />
11+
</vector>

0 commit comments

Comments
 (0)