-
Notifications
You must be signed in to change notification settings - Fork 13
feat: Add WebView integration tab for JS SDK #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Mansi-mParticle
wants to merge
1
commit into
main
Choose a base branch
from
feat/SDKE-792-tab-for-webview
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
189 changes: 189 additions & 0 deletions
189
core-sdk-samples/higgs-shop-sample-app/app/src/main/assets/webview_bridge_test.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>mParticle WebView Bridge Test</title> | ||
| <style> | ||
| body { font-family: sans-serif; background: #111; color: #fff; padding: 16px; } | ||
| button { padding: 12px 14px; margin-right: 10px; border-radius: 10px; border: 0; background: #4079fe; color: #fff; font-weight: 600; } | ||
| .muted { opacity: 0.7; } | ||
| </style> | ||
| <script type="text/javascript"> | ||
| // WebView-only snippet (NO API KEY) + required bridge name. | ||
| const mySdkConfig = { | ||
| requiredWebviewBridgeName: "higgsWebviewBridge", | ||
| isDevelopmentMode: true | ||
| }; | ||
| window.mParticle = { config: mySdkConfig }; | ||
|
|
||
| (function () { | ||
| window.mParticle = window.mParticle || { | ||
| EventType: { Unknown: 0, Navigation: 1, Location: 2, Search: 3, Transaction: 4, UserContent: 5, UserPreference: 6, Social: 7, Other: 8 } | ||
| }; | ||
| window.mParticle.eCommerce = { Cart: {} }; | ||
| window.mParticle.Identity = {}; | ||
| window.mParticle.config = window.mParticle.config || {}; | ||
| window.mParticle.config.rq = []; | ||
| window.mParticle.ready = function (t) { window.mParticle.config.rq.push(t); }; | ||
| function e(e, o) { | ||
| return function () { | ||
| if (o) { e = o + "." + e; } | ||
| var t = Array.prototype.slice.call(arguments); | ||
| t.unshift(e); | ||
| window.mParticle.config.rq.push(t); | ||
| }; | ||
| } | ||
| var o = ["endSession", "logError", "logEvent", "logForm", "logLink", "logPageView", "setSessionAttribute", "setAppName", "setAppVersion", "setOptOut", "setPosition", "startNewSession", "startTrackingLocation", "stopTrackingLocation"]; | ||
| var n = ["setCurrencyCode", "logCheckout"]; | ||
| var i = ["identify", "login", "logout", "modify"]; | ||
| o.forEach(function (t) { window.mParticle[t] = e(t); }); | ||
| n.forEach(function (t) { window.mParticle.eCommerce[t] = e(t, "eCommerce"); }); | ||
| i.forEach(function (t) { window.mParticle.Identity[t] = e(t, "Identity"); }); | ||
| var r = document.createElement("script"); | ||
| r.type = "text/javascript"; | ||
| r.async = true; | ||
| r.src = "https://jssdkcdns.mparticle.com/js/v2/mparticle.js"; | ||
| var c = document.getElementsByTagName("script")[0]; | ||
| c.parentNode.insertBefore(r, c); | ||
| })(); | ||
| </script> | ||
| </head> | ||
| <body> | ||
| <h2>mParticle WebView Bridge</h2> | ||
| <p class="muted"> | ||
| This page is loaded from <code>file:///android_asset</code> and uses the Web SDK in bridged mode. | ||
| Click the button to log a JS event that should be forwarded through the Android SDK. | ||
| </p> | ||
|
|
||
| <button id="btnEvent">Log JS Event</button> | ||
| <button id="btnPage">Log Page View</button> | ||
|
|
||
| <hr style="border:0;border-top:1px solid rgba(255,255,255,0.15);margin:18px 0;" /> | ||
|
|
||
| <h3 style="margin:0 0 10px 0;">Identity Alias (JS → Native)</h3> | ||
| <p class="muted" style="margin-top:0;"> | ||
| This calls <code>mParticle.Identity.aliasUsers</code>. In WebView bridge mode this is queued as a JS request and | ||
| forwarded to the Android SDK. We auto-populate a valid aliasRequest from the native SDK (MPIDs + time window), | ||
| similar to the docs reference. | ||
| </p> | ||
|
|
||
| <div style="display:flex;flex-direction:column;gap:10px;max-width:520px;"> | ||
| <div class="muted" id="aliasInfo">Waiting for native MPIDs…</div> | ||
| <div> | ||
| <button id="btnAlias" disabled style="opacity:0.6;">Send Alias Users</button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <script> | ||
| function log(msg) { | ||
| // Keep logs out of the UI; use console instead. | ||
| console.log((new Date().toISOString()) + " " + msg); | ||
| } | ||
|
|
||
| document.getElementById("btnEvent").addEventListener("click", function () { | ||
| if (!window.mParticle) return log("mParticle not available"); | ||
| window.mParticle.logEvent( | ||
| "WebView JS Button Click", | ||
| window.mParticle.EventType.Other, | ||
| { source: "webview", bridgeName: "higgsWebviewBridge" } | ||
| ); | ||
| log("Called mParticle.logEvent('WebView JS Button Click')"); | ||
| }); | ||
|
|
||
| document.getElementById("btnPage").addEventListener("click", function () { | ||
| if (!window.mParticle) return log("mParticle not available"); | ||
| window.mParticle.logPageView("WebView Bridge Test", { source: "webview" }); | ||
| log("Called mParticle.logPageView('WebView Bridge Test')"); | ||
| }); | ||
|
|
||
| window.__mp = window.__mp || {}; | ||
| window.__mp.aliasRequest = window.__mp.aliasRequest || null; | ||
|
|
||
| // Called by native via evaluateJavascript after page load | ||
| window.setNativeAliasRequest = function (aliasRequest) { | ||
| window.__mp.aliasRequest = aliasRequest || null; | ||
| const info = document.getElementById("aliasInfo"); | ||
| const btn = document.getElementById("btnAlias"); | ||
| if (!window.__mp.aliasRequest) { | ||
| info.textContent = "Missing aliasRequest from native SDK"; | ||
| btn.disabled = true; | ||
| btn.style.opacity = "0.6"; | ||
| return; | ||
| } | ||
| const ar = window.__mp.aliasRequest; | ||
| const valid = !!ar.sourceMpid && !!ar.destinationMpid && String(ar.sourceMpid) !== String(ar.destinationMpid); | ||
| info.textContent = | ||
| `Using sourceMpid=${ar.sourceMpid} → destinationMpid=${ar.destinationMpid} ` + | ||
| `(scope=${ar.scope || "device"})`; | ||
| btn.disabled = !valid; | ||
| btn.style.opacity = valid ? "1" : "0.6"; | ||
| if (!valid) { | ||
| info.textContent = "Invalid MPIDs from native SDK (need 2 unique users)"; | ||
| } | ||
| }; | ||
|
|
||
| // If native injected a pending request before this function was defined, apply it now. | ||
| if (window.__mp && window.__mp._pendingAliasRequest) { | ||
| window.setNativeAliasRequest(window.__mp._pendingAliasRequest); | ||
| } | ||
|
|
||
| // Log screen view from JS (not native) when page loads | ||
| window.mParticle.ready(function() { | ||
| if (window.mParticle && typeof window.mParticle.logScreen === "function") { | ||
| window.mParticle.logScreen("WebView Bridge"); | ||
| log("Called mParticle.logScreen('WebView Bridge') from JS"); | ||
| } | ||
| }); | ||
|
|
||
| document.getElementById("btnAlias").addEventListener("click", function () { | ||
| // Send alias request from WebView (JS) per Web SDK docs: | ||
| // https://docs.mparticle.com/developers/client-sdks/web/idsync/#user-aliasing | ||
| if (!window.mParticle || !window.mParticle.Identity) return log("mParticle.Identity not available"); | ||
| if (typeof window.mParticle.Identity.aliasUsers !== "function") return log("mParticle.Identity.aliasUsers not available yet"); | ||
| if (!window.__mp.aliasRequest) return log("Alias blocked: missing aliasRequest (native injection not ready)"); | ||
|
|
||
| const sourceMpidStr = String(window.__mp.aliasRequest.sourceMpid || "").trim(); | ||
| const destinationMpidStr = String(window.__mp.aliasRequest.destinationMpid || "").trim(); | ||
|
|
||
| if (!sourceMpidStr || !destinationMpidStr) { | ||
| return log("Alias blocked: missing MPIDs"); | ||
| } | ||
| if (sourceMpidStr === destinationMpidStr) { | ||
| return log("Alias blocked: sourceMpid and destinationMpid must be unique"); | ||
| } | ||
|
|
||
| // Parse MPIDs as numbers for the bridge (may lose precision for very large MPIDs, | ||
| // but the bridge might require numeric values to convert to native long). | ||
| const sourceMpidNum = Number(sourceMpidStr); | ||
| const destinationMpidNum = Number(destinationMpidStr); | ||
|
|
||
| if (!Number.isFinite(sourceMpidNum) || !Number.isFinite(destinationMpidNum)) { | ||
| return log(`Alias blocked: MPIDs not numeric (source=${sourceMpidStr}, dest=${destinationMpidStr})`); | ||
| } | ||
| if (sourceMpidNum <= 0 || destinationMpidNum <= 0) { | ||
| return log(`Alias blocked: MPIDs must be positive (source=${sourceMpidNum}, dest=${destinationMpidNum})`); | ||
| } | ||
|
|
||
| const now = Date.now(); | ||
| // Per Web SDK docs format: MPIDs as numbers, times as numbers (ms since epoch) | ||
| const aliasRequest = { | ||
| sourceMpid: sourceMpidNum, | ||
| destinationMpid: destinationMpidNum, | ||
| startTime: now - (30 * 60 * 1000), // last 30 minutes | ||
| endTime: now, | ||
| scope: "device" | ||
| }; | ||
|
|
||
| log(`Sending aliasRequest: sourceMpid=${sourceMpidNum} (from "${sourceMpidStr}"), destinationMpid=${destinationMpidNum} (from "${destinationMpidStr}")`); | ||
|
|
||
| window.mParticle.Identity.aliasUsers(aliasRequest, function (code, body) { | ||
| log(`aliasUsers callback: code=${code} body=${JSON.stringify(body)}`); | ||
| }); | ||
| log(`Called mParticle.Identity.aliasUsers(${JSON.stringify(aliasRequest)})`); | ||
| }); | ||
|
|
||
| log("Loaded. Waiting for user action…"); | ||
| </script> | ||
| </body> | ||
| </html> | ||
152 changes: 152 additions & 0 deletions
152
...c/main/kotlin/com/mparticle/example/higgsshopsampleapp/fragments/WebViewBridgeFragment.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| package com.mparticle.example.higgsshopsampleapp.fragments | ||
|
|
||
| import android.os.Bundle | ||
| import android.util.Log | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import android.webkit.WebChromeClient | ||
| import android.webkit.WebView | ||
| import android.webkit.WebViewClient | ||
| import androidx.fragment.app.Fragment | ||
| import com.mparticle.MParticle | ||
| import com.mparticle.identity.AliasRequest | ||
| import com.mparticle.example.higgsshopsampleapp.R | ||
| import com.mparticle.example.higgsshopsampleapp.activities.MainActivity | ||
| import com.mparticle.example.higgsshopsampleapp.databinding.FragmentWebviewBridgeBinding | ||
|
|
||
| class WebViewBridgeFragment : Fragment() { | ||
|
|
||
| private lateinit var binding: FragmentWebviewBridgeBinding | ||
|
|
||
| private val tag = "WebViewBridgeFragment" | ||
| private val bridgeName = "higgsWebviewBridge" | ||
| private val assetUrl = "file:///android_asset/webview_bridge_test.html" | ||
|
|
||
| override fun onCreateView( | ||
| inflater: LayoutInflater, | ||
| container: ViewGroup?, | ||
| savedInstanceState: Bundle? | ||
| ): View { | ||
| (activity as MainActivity).setActionBarTitle("") | ||
| inflater.context.setTheme(R.style.Theme_mParticle_SampleApp) | ||
| binding = FragmentWebviewBridgeBinding.inflate(inflater, container, false) | ||
| return binding.root | ||
| } | ||
|
|
||
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
| // All events should come from JS, not native - screen view will be logged from WebView | ||
|
|
||
| binding.btnAliasNative.setOnClickListener { | ||
| aliasUsersNative() | ||
| } | ||
|
|
||
| configureWebView(binding.webview) | ||
| // Per docs, the WebView must be registered before loading content. | ||
| MParticle.getInstance()?.registerWebView(binding.webview, bridgeName) | ||
| binding.webview.loadUrl(assetUrl) | ||
| } | ||
|
|
||
| private fun configureWebView(webView: WebView) { | ||
| webView.settings.javaScriptEnabled = true | ||
| webView.settings.domStorageEnabled = true | ||
| webView.settings.allowFileAccess = true | ||
| webView.settings.allowContentAccess = true | ||
|
|
||
| webView.webChromeClient = WebChromeClient() | ||
| webView.webViewClient = object : WebViewClient() { | ||
| override fun onPageFinished(view: WebView, url: String) { | ||
| super.onPageFinished(view, url) | ||
| injectAliasRequestIntoPage(view) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Inject MPIDs into the page so the WebView can send alias requests via mParticle.Identity.aliasUsers() | ||
| */ | ||
| private fun injectAliasRequestIntoPage(webView: WebView) { | ||
| val identity = MParticle.getInstance()?.Identity() ?: return | ||
| val users = try { | ||
| identity.users | ||
| } catch (_: Throwable) { | ||
| identity.getUsers() | ||
| } | ||
| if (users.size < 2) return | ||
|
|
||
| // Match docs: users are in reverse chronological order: | ||
| // source = users[1], destination = users[0] | ||
| val sourceUser = users[1] | ||
| val destinationUser = users[0] | ||
|
|
||
| val sourceMpid = try { | ||
| sourceUser.id | ||
| } catch (_: Throwable) { | ||
| sourceUser.getId() | ||
| }.toString() | ||
|
|
||
| val destinationMpid = try { | ||
| destinationUser.id | ||
| } catch (_: Throwable) { | ||
| destinationUser.getId() | ||
| }.toString() | ||
|
|
||
| Log.i(tag, "Mansi sourceMpid=$sourceMpid destinationMpid=$destinationMpid") | ||
|
|
||
| if (sourceMpid.isBlank() || destinationMpid.isBlank()) return | ||
| if (sourceMpid == destinationMpid) return | ||
|
|
||
| val now = System.currentTimeMillis() | ||
| val startTime = now - (30 * 60 * 1000) | ||
| val endTime = now | ||
|
|
||
| val js = | ||
| "window.__mp = window.__mp || {};" + | ||
| "window.__mp._pendingAliasRequest = {sourceMpid:'$sourceMpid',destinationMpid:'$destinationMpid',startTime:$startTime,endTime:$endTime,scope:'device'};" + | ||
| "if (window.setNativeAliasRequest) { window.setNativeAliasRequest(window.__mp._pendingAliasRequest); }" | ||
| webView.evaluateJavascript(js, null) | ||
| } | ||
|
|
||
| /** | ||
| * Native alias invoked from the fragment button (and from the WebView bridge). | ||
| * Uses the "working" approach: source=users[1], destination=users[0], time window = last 30 minutes. | ||
| */ | ||
|
|
||
| // Kept native button as-is (optional): it calls the Android SDK alias directly. | ||
| private fun aliasUsersNative() { | ||
| val identity = MParticle.getInstance()?.Identity() ?: return | ||
| val users = try { | ||
| identity.users | ||
| } catch (_: Throwable) { | ||
| identity.getUsers() | ||
| } | ||
| if (users.size < 2) { | ||
| binding.subtitle.text = getString(R.string.webview_bridge_alias_need_two_users) | ||
| return | ||
| } | ||
| val sourceUser = users[1] | ||
| val destinationUser = users[0] | ||
| val now = System.currentTimeMillis() | ||
| val request: AliasRequest = AliasRequest.Builder() | ||
| .sourceMpid(sourceUser.id.toString().toLongOrNull() ?: 0L) | ||
| .destinationMpid(destinationUser.id.toString().toLongOrNull() ?: 0L) | ||
| .startTime(now - (30 * 60 * 1000)) | ||
| .endTime(now) | ||
| .build() | ||
| identity.aliasUsers(request) | ||
| } | ||
|
|
||
| override fun onDestroyView() { | ||
| // Avoid leaking the WebView. | ||
| binding.webview.apply { | ||
| stopLoading() | ||
| loadUrl("about:blank") | ||
| clearHistory() | ||
| removeAllViews() | ||
| destroy() | ||
| } | ||
| super.onDestroyView() | ||
| } | ||
| } | ||
|
|
11 changes: 11 additions & 0 deletions
11
core-sdk-samples/higgs-shop-sample-app/app/src/main/res/drawable/icon_webview.xml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:width="24dp" | ||
| android:height="24dp" | ||
| android:tint="#FFFFFF" | ||
| android:viewportWidth="24" | ||
| android:viewportHeight="24"> | ||
| <!-- Material "public" (globe) --> | ||
| <path | ||
| android:fillColor="@android:color/white" | ||
| 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" /> | ||
| </vector> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this
(NO API KEY) + required bridge name?