react-native-hubspot-wrapper 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -12
- package/android/src/main/java/com/reactnativehubspotwrapper/HubspotChatScriptInstaller.kt +232 -0
- package/android/src/main/java/com/reactnativehubspotwrapper/HubspotWrapperModule.kt +6 -6
- package/ios/HubspotMobileSDK/Views/ChatView/HubspotChatView.swift +0 -3
- package/ios/HubspotWrapperImpl.swift +15 -214
- package/package.json +1 -1
- package/scripts/update-hubspot-ios-sdk.sh +0 -24
- package/src/index.ts +4 -1
- package/android/src/main/java/com/reactnativehubspotwrapper/HubspotBackButtonHider.kt +0 -279
package/README.md
CHANGED
|
@@ -61,15 +61,9 @@ await HubspotWrapper.setProperties([{ name: 'plan', value: 'pro' }]);
|
|
|
61
61
|
await HubspotWrapper.openChat('support');
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
await HubspotWrapper.openChat('support', {
|
|
70
|
-
hideBackToInboxButton: false,
|
|
71
|
-
});
|
|
72
|
-
```
|
|
64
|
+
`openChat` keeps HubSpot's stock conversations navigation visible. Older
|
|
65
|
+
versions accepted `hideBackToInboxButton`; the option is still accepted for
|
|
66
|
+
compatibility, but it no longer changes the HubSpot UI.
|
|
73
67
|
|
|
74
68
|
## API
|
|
75
69
|
|
|
@@ -83,9 +77,7 @@ await HubspotWrapper.openChat('support', {
|
|
|
83
77
|
|
|
84
78
|
Opens the HubSpot chat UI for the provided chatflow.
|
|
85
79
|
|
|
86
|
-
- `hideBackToInboxButton`
|
|
87
|
-
- Set `hideBackToInboxButton: false` to keep HubSpot's default conversations
|
|
88
|
-
navigation visible.
|
|
80
|
+
- `hideBackToInboxButton` is deprecated and ignored.
|
|
89
81
|
|
|
90
82
|
### `clearUserData()`
|
|
91
83
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
package com.marcinolek.reactnativehubspotwrapper
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
5
|
+
import android.os.Bundle
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.Looper
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import android.view.View
|
|
10
|
+
import android.view.ViewGroup
|
|
11
|
+
import android.webkit.JavascriptInterface
|
|
12
|
+
import android.webkit.WebView
|
|
13
|
+
import java.lang.ref.WeakReference
|
|
14
|
+
import java.util.WeakHashMap
|
|
15
|
+
import org.json.JSONObject
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Installs the small JavaScript bridge we need inside HubSpot's compiled
|
|
19
|
+
* [HubspotWebActivity], which cannot be modified directly from the wrapper.
|
|
20
|
+
*
|
|
21
|
+
* HubSpot Android SDK 1.0.8 uploads chat properties only after its native JS
|
|
22
|
+
* bridge receives a conversation id. The bundled script forwards
|
|
23
|
+
* `userSelectedThread`, but not `conversationStarted`, so newly-created threads
|
|
24
|
+
* never trigger the metadata POST. This installer forwards that one event
|
|
25
|
+
* without changing HubSpot's visible chat UI.
|
|
26
|
+
*/
|
|
27
|
+
internal object HubspotChatScriptInstaller {
|
|
28
|
+
private const val TAG = "HubspotWrapper"
|
|
29
|
+
private const val JS_BRIDGE_NAME = "rnHubspotWrapper"
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The fully-qualified name of HubSpot's chat activity in the AAR. Matched by
|
|
33
|
+
* string so we don't have to import / depend on it for compile-time resolution.
|
|
34
|
+
*/
|
|
35
|
+
private const val WEB_ACTIVITY_NAME = "com.hubspot.mobilesdk.HubspotWebActivity"
|
|
36
|
+
|
|
37
|
+
/** How often we re-fire `evaluateJavascript` to catch late-loaded iframes. */
|
|
38
|
+
private const val RESCAN_INTERVAL_MS = 250L
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Total time we keep re-firing after the activity resumes. The chat iframe
|
|
42
|
+
* normally appears within ~2s; we give a generous margin and then stop to
|
|
43
|
+
* avoid burning CPU forever if the user leaves the chat open for a long time.
|
|
44
|
+
*/
|
|
45
|
+
private const val MAX_SCAN_DURATION_MS = 10_000L
|
|
46
|
+
|
|
47
|
+
@Volatile
|
|
48
|
+
private var installed = false
|
|
49
|
+
|
|
50
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
51
|
+
private val scheduledInjections = WeakHashMap<WebView, Runnable>()
|
|
52
|
+
private val conversationBridges = WeakHashMap<WebView, ConversationBridge>()
|
|
53
|
+
|
|
54
|
+
@Synchronized
|
|
55
|
+
fun installOnce(app: Application) {
|
|
56
|
+
if (installed) return
|
|
57
|
+
installed = true
|
|
58
|
+
|
|
59
|
+
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
|
|
60
|
+
override fun onActivityResumed(activity: Activity) {
|
|
61
|
+
if (activity.javaClass.name != WEB_ACTIVITY_NAME) return
|
|
62
|
+
val webView = findWebView(activity.window.decorView)
|
|
63
|
+
if (webView == null) {
|
|
64
|
+
Log.w(TAG, "installChatScripts: WebView not found in $WEB_ACTIVITY_NAME view tree")
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
Log.i(TAG, "installChatScripts: scheduling conversation bridge on $webView")
|
|
68
|
+
installNativeBridge(webView)
|
|
69
|
+
scheduleScriptInjection(webView)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
|
73
|
+
override fun onActivityStarted(activity: Activity) {}
|
|
74
|
+
override fun onActivityPaused(activity: Activity) {
|
|
75
|
+
if (activity.javaClass.name == WEB_ACTIVITY_NAME) {
|
|
76
|
+
cancelScriptInjection(findWebView(activity.window.decorView))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
override fun onActivityStopped(activity: Activity) {}
|
|
80
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
81
|
+
override fun onActivityDestroyed(activity: Activity) {
|
|
82
|
+
if (activity.javaClass.name == WEB_ACTIVITY_NAME) {
|
|
83
|
+
val webView = findWebView(activity.window.decorView)
|
|
84
|
+
cancelScriptInjection(webView)
|
|
85
|
+
if (webView != null) {
|
|
86
|
+
conversationBridges.remove(webView)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private fun findWebView(view: View?): WebView? {
|
|
94
|
+
if (view is WebView) return view
|
|
95
|
+
if (view is ViewGroup) {
|
|
96
|
+
for (i in 0 until view.childCount) {
|
|
97
|
+
val found = findWebView(view.getChildAt(i))
|
|
98
|
+
if (found != null) return found
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun scheduleScriptInjection(webView: WebView) {
|
|
105
|
+
cancelScriptInjection(webView)
|
|
106
|
+
|
|
107
|
+
val start = System.currentTimeMillis()
|
|
108
|
+
val runnable = object : Runnable {
|
|
109
|
+
override fun run() {
|
|
110
|
+
try {
|
|
111
|
+
webView.evaluateJavascript(CONVERSATION_STARTED_BRIDGE_JS, null)
|
|
112
|
+
} catch (error: Exception) {
|
|
113
|
+
Log.w(TAG, "evaluateJavascript failed", error)
|
|
114
|
+
}
|
|
115
|
+
if (System.currentTimeMillis() - start < MAX_SCAN_DURATION_MS) {
|
|
116
|
+
handler.postDelayed(this, RESCAN_INTERVAL_MS)
|
|
117
|
+
} else {
|
|
118
|
+
scheduledInjections.remove(webView)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
scheduledInjections[webView] = runnable
|
|
123
|
+
handler.post(runnable)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fun installNativeBridge(webView: WebView) {
|
|
127
|
+
if (conversationBridges.containsKey(webView)) return
|
|
128
|
+
val bridge = ConversationBridge(WeakReference(webView))
|
|
129
|
+
conversationBridges[webView] = bridge
|
|
130
|
+
webView.addJavascriptInterface(bridge, JS_BRIDGE_NAME)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun cancelScriptInjection(webView: WebView?) {
|
|
134
|
+
if (webView == null) return
|
|
135
|
+
val runnable = scheduledInjections.remove(webView) ?: return
|
|
136
|
+
handler.removeCallbacks(runnable)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun forwardConversationIdToNativeApp(webView: WebView, conversationId: String) {
|
|
140
|
+
val quotedId = JSONObject.quote(conversationId)
|
|
141
|
+
handler.post {
|
|
142
|
+
try {
|
|
143
|
+
webView.evaluateJavascript(
|
|
144
|
+
"""
|
|
145
|
+
(function() {
|
|
146
|
+
if (window.nativeApp && typeof window.nativeApp.postConversationId === 'function') {
|
|
147
|
+
window.nativeApp.postConversationId($quotedId);
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
""".trimIndent(),
|
|
151
|
+
null
|
|
152
|
+
)
|
|
153
|
+
} catch (error: Exception) {
|
|
154
|
+
Log.w(TAG, "postConversationId bridge failed", error)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private class ConversationBridge(private val webViewRef: WeakReference<WebView>) {
|
|
160
|
+
@JavascriptInterface
|
|
161
|
+
fun postConversationId(conversationId: String?) {
|
|
162
|
+
val id = conversationId?.takeIf { it.isNotBlank() } ?: return
|
|
163
|
+
val webView = webViewRef.get() ?: return
|
|
164
|
+
forwardConversationIdToNativeApp(webView, id)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private const val CONVERSATION_STARTED_BRIDGE_JS = """
|
|
169
|
+
(function() {
|
|
170
|
+
function bootstrap(win) {
|
|
171
|
+
var doc;
|
|
172
|
+
try { doc = win.document; } catch (_) { return; }
|
|
173
|
+
if (!doc) return;
|
|
174
|
+
|
|
175
|
+
if (doc.__rnHubspotConversationStartedBridgeInstalled) return;
|
|
176
|
+
doc.__rnHubspotConversationStartedBridgeInstalled = true;
|
|
177
|
+
|
|
178
|
+
function extractConversationId(payload) {
|
|
179
|
+
try {
|
|
180
|
+
var conversation = payload && payload.conversation;
|
|
181
|
+
var id = conversation && (
|
|
182
|
+
conversation.conversationId ||
|
|
183
|
+
conversation.threadId ||
|
|
184
|
+
conversation.id
|
|
185
|
+
);
|
|
186
|
+
if (id === undefined || id === null || id === '') return null;
|
|
187
|
+
return String(id);
|
|
188
|
+
} catch (_) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function postConversationId(payload) {
|
|
194
|
+
var id = extractConversationId(payload);
|
|
195
|
+
if (!id) return;
|
|
196
|
+
try {
|
|
197
|
+
if (win.rnHubspotWrapper && typeof win.rnHubspotWrapper.postConversationId === 'function') {
|
|
198
|
+
win.rnHubspotWrapper.postConversationId(id);
|
|
199
|
+
}
|
|
200
|
+
} catch (_) {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function configureHubspotConversations() {
|
|
204
|
+
if (!win.HubSpotConversations) return;
|
|
205
|
+
try {
|
|
206
|
+
win.HubSpotConversations.on('conversationStarted', postConversationId);
|
|
207
|
+
} catch (_) {}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (win.HubSpotConversations) {
|
|
211
|
+
configureHubspotConversations();
|
|
212
|
+
} else if (Array.isArray(win.hsConversationsOnReady)) {
|
|
213
|
+
win.hsConversationsOnReady.push(configureHubspotConversations);
|
|
214
|
+
} else {
|
|
215
|
+
win.hsConversationsOnReady = [configureHubspotConversations];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function walkAllFrames(win) {
|
|
220
|
+
if (!win) return;
|
|
221
|
+
bootstrap(win);
|
|
222
|
+
try {
|
|
223
|
+
for (var i = 0; i < win.frames.length; i++) {
|
|
224
|
+
walkAllFrames(win.frames[i]);
|
|
225
|
+
}
|
|
226
|
+
} catch (_) {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
walkAllFrames(window);
|
|
230
|
+
})();
|
|
231
|
+
"""
|
|
232
|
+
}
|
|
@@ -22,11 +22,11 @@ class HubspotWrapperModule(reactContext: ReactApplicationContext) :
|
|
|
22
22
|
private lateinit var hubspotManager: HubspotManager
|
|
23
23
|
|
|
24
24
|
init {
|
|
25
|
-
// Install the chat
|
|
26
|
-
// The
|
|
25
|
+
// Install the chat script bridge as soon as the wrapper module is created.
|
|
26
|
+
// The bridge only does work when `HubspotWebActivity` actually resumes, so this
|
|
27
27
|
// is a cheap one-time `Application.ActivityLifecycleCallbacks` registration.
|
|
28
|
-
// See `
|
|
29
|
-
(appContext as? Application)?.let {
|
|
28
|
+
// See `HubspotChatScriptInstaller` for why this lives outside the activity itself.
|
|
29
|
+
(appContext as? Application)?.let { HubspotChatScriptInstaller.installOnce(it) }
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
override fun getName(): String = NAME
|
|
@@ -42,12 +42,12 @@ class HubspotWrapperModule(reactContext: ReactApplicationContext) :
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
@Suppress("UNUSED_PARAMETER")
|
|
45
46
|
override fun openChat(chatflow: String, hideBackToInboxButton: Boolean, promise: Promise) {
|
|
46
47
|
try {
|
|
47
48
|
val intent = Intent(appContext, HubspotWebActivity::class.java)
|
|
48
49
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
49
50
|
intent.putExtra("chatflow", chatflow)
|
|
50
|
-
intent.putExtra(HubspotBackButtonHider.EXTRA_HIDE_BACK_TO_INBOX_BUTTON, hideBackToInboxButton)
|
|
51
51
|
appContext.startActivity(intent)
|
|
52
52
|
promise.resolve(null)
|
|
53
53
|
} catch (error: Exception) {
|
|
@@ -57,7 +57,7 @@ class HubspotWrapperModule(reactContext: ReactApplicationContext) :
|
|
|
57
57
|
|
|
58
58
|
override fun setIdentity(identityToken: String, email: String?, promise: Promise) {
|
|
59
59
|
try {
|
|
60
|
-
hubspotManager.setUserIdentity(
|
|
60
|
+
hubspotManager.setUserIdentity(email ?: "", identityToken)
|
|
61
61
|
promise.resolve(null)
|
|
62
62
|
} catch (error: Exception) {
|
|
63
63
|
promise.reject("IDENTITY_ERROR", "Failed to set HubSpot identity", error)
|
|
@@ -349,9 +349,6 @@ struct HubspotChatWebView: UIViewRepresentable {
|
|
|
349
349
|
|
|
350
350
|
contentController.addUserScript(WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: false))
|
|
351
351
|
|
|
352
|
-
// [react-native-hubspot-wrapper] hide chat "back to inbox" button - see HubspotWrapperImpl
|
|
353
|
-
HubspotWrapperImpl.installBackButtonHider(on: contentController)
|
|
354
|
-
|
|
355
352
|
// create script that triggers on hubspot event, and calls our message handler
|
|
356
353
|
|
|
357
354
|
let configCallbacksJS = """
|
|
@@ -5,27 +5,6 @@ import WebKit
|
|
|
5
5
|
|
|
6
6
|
@objcMembers
|
|
7
7
|
public class HubspotWrapperImpl: NSObject {
|
|
8
|
-
private static var shouldHideBackToInboxButton = true
|
|
9
|
-
|
|
10
|
-
/// Substrings used to match `WKWebsiteDataRecord.displayName` (which is the host /
|
|
11
|
-
/// eTLD+1, e.g. `hubspot.com`, `hs-banner.com`, `hsadspixel.net`) for the various
|
|
12
|
-
/// domains the chat widget loads from. Anything matching is wiped on `clearUserData`.
|
|
13
|
-
///
|
|
14
|
-
/// We deliberately use substrings rather than exact hosts because the widget pulls
|
|
15
|
-
/// resources from a long tail of HubSpot CDNs - `js.hs-banner.com`, `js.hubspot.com`,
|
|
16
|
-
/// `static.hsappstatic.net`, `app-na2.hubspot.com`, `*.hubspotqa.com`, etc. - and any
|
|
17
|
-
/// of those can hold cookies / localStorage / IndexedDB that re-attach a returning
|
|
18
|
-
/// visitor or surface the unsent draft message on the next open.
|
|
19
|
-
private static let chatDataDomainMatches: [String] = [
|
|
20
|
-
"hubspot",
|
|
21
|
-
"hsforms",
|
|
22
|
-
"hs-banner",
|
|
23
|
-
"hs-scripts",
|
|
24
|
-
"hsadspixel",
|
|
25
|
-
"hsappstatic",
|
|
26
|
-
"usemessages",
|
|
27
|
-
]
|
|
28
|
-
|
|
29
8
|
public func initialize(_ outError: NSErrorPointer) -> Bool {
|
|
30
9
|
let semaphore = DispatchSemaphore(value: 0)
|
|
31
10
|
var configureError: NSError?
|
|
@@ -48,7 +27,7 @@ public class HubspotWrapperImpl: NSObject {
|
|
|
48
27
|
return true
|
|
49
28
|
}
|
|
50
29
|
|
|
51
|
-
public func openChat(_ chatflow: String, hideBackToInboxButton: Bool, error outError: NSErrorPointer) -> Bool {
|
|
30
|
+
public func openChat(_ chatflow: String, hideBackToInboxButton _: Bool, error outError: NSErrorPointer) -> Bool {
|
|
52
31
|
var didSucceed = false
|
|
53
32
|
let presentBlock = {
|
|
54
33
|
guard let rootVC = Self.topViewController() else {
|
|
@@ -60,7 +39,6 @@ public class HubspotWrapperImpl: NSObject {
|
|
|
60
39
|
return
|
|
61
40
|
}
|
|
62
41
|
|
|
63
|
-
Self.shouldHideBackToInboxButton = hideBackToInboxButton
|
|
64
42
|
let chatView = HubspotChatView(chatFlow: chatflow)
|
|
65
43
|
let hostingController = UIHostingController(rootView: chatView)
|
|
66
44
|
rootVC.present(hostingController, animated: true)
|
|
@@ -103,10 +81,8 @@ public class HubspotWrapperImpl: NSObject {
|
|
|
103
81
|
}
|
|
104
82
|
}
|
|
105
83
|
|
|
106
|
-
/// Clear the SDK's in-memory identity/property state and synchronously wait for
|
|
107
|
-
///
|
|
108
|
-
/// workers, etc.) to be removed from the shared `WKWebsiteDataStore` before invoking
|
|
109
|
-
/// `completion`.
|
|
84
|
+
/// Clear the SDK's in-memory identity/property state and synchronously wait for HubSpot's
|
|
85
|
+
/// visitor-identity cookies to be removed before invoking `completion`.
|
|
110
86
|
///
|
|
111
87
|
/// Why we do our own clearing instead of relying on the SDK:
|
|
112
88
|
///
|
|
@@ -117,203 +93,28 @@ public class HubspotWrapperImpl: NSObject {
|
|
|
117
93
|
/// races the still-in-flight cookie deletion, so the next chat session re-uses
|
|
118
94
|
/// the previous visitor identity.
|
|
119
95
|
///
|
|
120
|
-
///
|
|
121
|
-
///
|
|
122
|
-
///
|
|
123
|
-
/// `sessionStorage` / `IndexedDB`. Cookie-only clearing leaves the draft visible
|
|
124
|
-
/// on the next open, which is exactly the bug we kept seeing.
|
|
125
|
-
///
|
|
126
|
-
/// We therefore enumerate `WKWebsiteDataStore` records, filter to records whose
|
|
127
|
-
/// host matches a HubSpot domain (see `chatDataDomainMatches`) and remove ALL data
|
|
128
|
-
/// types for those records. We only resolve the JS promise once that's complete.
|
|
96
|
+
/// Broader WebKit website-data cleanup is intentionally avoided here. Both
|
|
97
|
+
/// `dataRecords(ofTypes:)` and `removeData(ofTypes:modifiedSince:)` have crashed in
|
|
98
|
+
/// production inside WebKit's `allDataStores()` path on iOS 18 simulators.
|
|
129
99
|
public func clearUserData(_ completion: @escaping () -> Void) {
|
|
130
100
|
Task { @MainActor in
|
|
131
101
|
HubspotManager.shared.clearUserData()
|
|
132
|
-
await Self.
|
|
102
|
+
await Self.deleteHubspotIdentityCookies()
|
|
133
103
|
completion()
|
|
134
104
|
}
|
|
135
105
|
}
|
|
136
106
|
|
|
137
|
-
private static func
|
|
138
|
-
let
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
107
|
+
private static func deleteHubspotIdentityCookies() async {
|
|
108
|
+
let cookieStore = WKWebsiteDataStore.default().httpCookieStore
|
|
109
|
+
let matchingCookies = await cookieStore.allCookies().filter {
|
|
110
|
+
hubspotIdentityCookieNames.contains($0.name)
|
|
111
|
+
}
|
|
112
|
+
for cookie in matchingCookies {
|
|
113
|
+
await cookieStore.deleteCookie(cookie)
|
|
144
114
|
}
|
|
145
|
-
guard !matching.isEmpty else { return }
|
|
146
|
-
await store.removeData(ofTypes: allTypes, for: matching)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/// Install a `WKUserScript` on `controller` that hides any "back to inbox" /
|
|
150
|
-
/// conversations-list navigation rendered by the HubSpot chat widget.
|
|
151
|
-
///
|
|
152
|
-
/// Called from the patched `HubspotChatWebView.WebviewCoordinator.setupScripts()`
|
|
153
|
-
/// (see `update-hubspot-ios-sdk.sh` for the patch that re-applies that hook on
|
|
154
|
-
/// every SDK sync). The actual heuristic-based hide JS lives here in our own
|
|
155
|
-
/// non-vendored file so SDK updates don't churn the implementation.
|
|
156
|
-
///
|
|
157
|
-
/// We can't rely on a single CSS selector because:
|
|
158
|
-
/// - HubSpot's widget uses minified/hashed class names that change across
|
|
159
|
-
/// releases.
|
|
160
|
-
/// - Parts of the widget render inside shadow roots, which a top-level
|
|
161
|
-
/// `<style>` doesn't pierce.
|
|
162
|
-
/// - The button can mount after first paint as the widget hydrates.
|
|
163
|
-
///
|
|
164
|
-
/// The injected script installs a `MutationObserver` that walks the full tree
|
|
165
|
-
/// (recursing into open shadow roots) and hides anything that *behaves* like a
|
|
166
|
-
/// back button - matched on `aria-label`, `data-test-id`, `title`, `id`, `class`,
|
|
167
|
-
/// and visible text. Re-scans periodically for the first few seconds to cover
|
|
168
|
-
/// late-mounted shadow roots. Injected at document start in every frame.
|
|
169
|
-
@objc public static func installBackButtonHider(on controller: WKUserContentController) {
|
|
170
|
-
guard shouldHideBackToInboxButton else { return }
|
|
171
|
-
|
|
172
|
-
let script = WKUserScript(
|
|
173
|
-
source: backButtonHiderJS,
|
|
174
|
-
injectionTime: .atDocumentStart,
|
|
175
|
-
forMainFrameOnly: false
|
|
176
|
-
)
|
|
177
|
-
controller.addUserScript(script)
|
|
178
115
|
}
|
|
179
116
|
|
|
180
|
-
|
|
181
|
-
/// work both as a `WKUserScript` (per-frame at `.atDocumentStart`) and as a
|
|
182
|
-
/// one-shot `evaluateJavascript` payload from the Android side - hence the
|
|
183
|
-
/// `walkAllFrames` / `bootstrap` split with the per-document install guard.
|
|
184
|
-
/// Keep this string in lockstep with `HubspotBackButtonHider.BACK_BUTTON_HIDER_JS`
|
|
185
|
-
/// in the Android wrapper. Insert spaces at camelCase boundaries and collapse
|
|
186
|
-
/// underscores/dashes to spaces so word-boundary matching catches class names
|
|
187
|
-
/// like `ConversationView__BackButton`. (In JS regex, `_` is a word char, so
|
|
188
|
-
/// without this `\\bback\\b` would fail against `..._back_...`.)
|
|
189
|
-
private static let backButtonHiderJS: String = """
|
|
190
|
-
(function() {
|
|
191
|
-
function bootstrap(win) {
|
|
192
|
-
var doc;
|
|
193
|
-
try { doc = win.document; } catch (_) { return; }
|
|
194
|
-
if (!doc || doc.__rnHubspotHiderInstalled) return;
|
|
195
|
-
doc.__rnHubspotHiderInstalled = true;
|
|
196
|
-
|
|
197
|
-
var BACK_PATTERN = /\\b(back|inbox|previous|conversations)\\b/i;
|
|
198
|
-
|
|
199
|
-
function normalize(s) {
|
|
200
|
-
if (!s) return '';
|
|
201
|
-
return String(s)
|
|
202
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
203
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
204
|
-
.replace(/[_-]+/g, ' ')
|
|
205
|
-
.toLowerCase();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function looksLikeBackControl(el) {
|
|
209
|
-
if (!el || el.nodeType !== 1) return false;
|
|
210
|
-
if (el.getAttribute('data-rn-hubspot-wrapper-hidden') === '1') return false;
|
|
211
|
-
var tag = el.tagName;
|
|
212
|
-
if (tag !== 'BUTTON' && tag !== 'A' && el.getAttribute('role') !== 'button') return false;
|
|
213
|
-
var attrs = [
|
|
214
|
-
el.getAttribute('aria-label'),
|
|
215
|
-
el.getAttribute('data-test-id'),
|
|
216
|
-
el.getAttribute('data-test'),
|
|
217
|
-
el.getAttribute('title'),
|
|
218
|
-
el.getAttribute('id'),
|
|
219
|
-
el.getAttribute('class')
|
|
220
|
-
];
|
|
221
|
-
for (var i = 0; i < attrs.length; i++) {
|
|
222
|
-
if (attrs[i] && BACK_PATTERN.test(normalize(attrs[i]))) return true;
|
|
223
|
-
}
|
|
224
|
-
var text = (el.textContent || '').trim();
|
|
225
|
-
if (text && text.length <= 32 && BACK_PATTERN.test(normalize(text))) return true;
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function hide(el) {
|
|
230
|
-
try {
|
|
231
|
-
el.setAttribute('data-rn-hubspot-wrapper-hidden', '1');
|
|
232
|
-
el.style.setProperty('display', 'none', 'important');
|
|
233
|
-
el.style.setProperty('visibility', 'hidden', 'important');
|
|
234
|
-
el.style.setProperty('pointer-events', 'none', 'important');
|
|
235
|
-
} catch (_) {}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Hide `el`, then walk up to a few ancestors and also hide any wrapper
|
|
239
|
-
// that becomes empty (no remaining unhidden element children) as a
|
|
240
|
-
// result. HubSpot's chat header has a back-button slot whose padding
|
|
241
|
-
// / min-width / flex-basis stays around even when the inner button is
|
|
242
|
-
// `display:none`, which shows up as the avatar shifting right after the
|
|
243
|
-
// first message is sent. Capping at depth 3 keeps us inside the header.
|
|
244
|
-
function hideUpwards(el) {
|
|
245
|
-
hide(el);
|
|
246
|
-
var current = el;
|
|
247
|
-
for (var depth = 0; depth < 3; depth++) {
|
|
248
|
-
var parent = current.parentElement;
|
|
249
|
-
if (!parent) break;
|
|
250
|
-
if (parent === doc.documentElement || parent === doc.body) break;
|
|
251
|
-
var visibleCount = 0;
|
|
252
|
-
var children = parent.children;
|
|
253
|
-
for (var i = 0; i < children.length; i++) {
|
|
254
|
-
if (children[i].getAttribute('data-rn-hubspot-wrapper-hidden') !== '1') {
|
|
255
|
-
visibleCount++;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (visibleCount > 0) break;
|
|
259
|
-
hide(parent);
|
|
260
|
-
current = parent;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
var observed = new WeakSet();
|
|
265
|
-
function scanRoot(root) {
|
|
266
|
-
if (!root) return;
|
|
267
|
-
try {
|
|
268
|
-
var nodes = root.querySelectorAll('button, a, [role="button"]');
|
|
269
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
270
|
-
if (looksLikeBackControl(nodes[i])) hideUpwards(nodes[i]);
|
|
271
|
-
}
|
|
272
|
-
var all = root.querySelectorAll('*');
|
|
273
|
-
for (var j = 0; j < all.length; j++) {
|
|
274
|
-
var sr = all[j].shadowRoot;
|
|
275
|
-
if (sr) {
|
|
276
|
-
scanRoot(sr);
|
|
277
|
-
attachObserver(sr);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
} catch (_) {}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function attachObserver(root) {
|
|
284
|
-
if (!root || observed.has(root)) return;
|
|
285
|
-
observed.add(root);
|
|
286
|
-
try {
|
|
287
|
-
var mo = new MutationObserver(function() { scanRoot(root); });
|
|
288
|
-
mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-label', 'data-test-id', 'data-test', 'title', 'class'] });
|
|
289
|
-
} catch (_) {}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function go() {
|
|
293
|
-
scanRoot(doc);
|
|
294
|
-
attachObserver(doc.documentElement || doc);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (doc.readyState === 'loading') {
|
|
298
|
-
doc.addEventListener('DOMContentLoaded', go, { once: true });
|
|
299
|
-
} else {
|
|
300
|
-
go();
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function walkAllFrames(win) {
|
|
305
|
-
if (!win) return;
|
|
306
|
-
bootstrap(win);
|
|
307
|
-
try {
|
|
308
|
-
for (var i = 0; i < win.frames.length; i++) {
|
|
309
|
-
walkAllFrames(win.frames[i]);
|
|
310
|
-
}
|
|
311
|
-
} catch (_) {}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
walkAllFrames(window);
|
|
315
|
-
})();
|
|
316
|
-
"""
|
|
117
|
+
private static let hubspotIdentityCookieNames = Set(["hubspotutk", "messagesUtk"])
|
|
317
118
|
|
|
318
119
|
private static func topViewController(
|
|
319
120
|
base: UIViewController? = UIApplication.shared.connectedScenes
|
package/package.json
CHANGED
|
@@ -76,30 +76,6 @@ if old_block not in content:
|
|
|
76
76
|
raise RuntimeError("Expected Image extension block was not found in HubspotManager.swift")
|
|
77
77
|
hubspot_manager.write_text(content.replace(old_block, new_block, 1))
|
|
78
78
|
|
|
79
|
-
# 4) Hook our back-button hider into HubspotChatWebView's WKUserContentController.
|
|
80
|
-
# The actual hider implementation lives in our own non-vendored
|
|
81
|
-
# HubspotWrapperImpl.swift; this patch just inserts a one-line call so the
|
|
82
|
-
# hider runs whenever the SDK builds its content controller. We open chat as
|
|
83
|
-
# a single fresh conversation per session and never want users navigating
|
|
84
|
-
# into prior threads.
|
|
85
|
-
chat_view = target_dir / "Views/ChatView/HubspotChatView.swift"
|
|
86
|
-
chat_content = chat_view.read_text()
|
|
87
|
-
anchor = (
|
|
88
|
-
'contentController.addUserScript(WKUserScript(source: js, '
|
|
89
|
-
'injectionTime: .atDocumentEnd, forMainFrameOnly: false))'
|
|
90
|
-
)
|
|
91
|
-
hook_block = (
|
|
92
|
-
"\n\n"
|
|
93
|
-
" // [react-native-hubspot-wrapper] hide chat \"back to inbox\" button - see HubspotWrapperImpl\n"
|
|
94
|
-
" HubspotWrapperImpl.installBackButtonHider(on: contentController)"
|
|
95
|
-
)
|
|
96
|
-
if anchor not in chat_content:
|
|
97
|
-
raise RuntimeError("Expected setupScripts anchor not found in HubspotChatView.swift")
|
|
98
|
-
if "HubspotWrapperImpl.installBackButtonHider" in chat_content:
|
|
99
|
-
# Already patched.
|
|
100
|
-
pass
|
|
101
|
-
else:
|
|
102
|
-
chat_view.write_text(chat_content.replace(anchor, anchor + hook_block, 1))
|
|
103
79
|
PY
|
|
104
80
|
}
|
|
105
81
|
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,9 @@ export type SetIdentityParams = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export type OpenChatOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated No-op. HubSpot's conversations navigation remains visible.
|
|
13
|
+
*/
|
|
11
14
|
hideBackToInboxButton?: boolean;
|
|
12
15
|
};
|
|
13
16
|
|
|
@@ -24,7 +27,7 @@ const HubspotWrapper = {
|
|
|
24
27
|
|
|
25
28
|
openChat(chatflow: string, options: OpenChatOptions = {}): Promise<void> {
|
|
26
29
|
ensureNonEmpty(chatflow, 'chatflow');
|
|
27
|
-
return NativeHubspotWrapper.openChat(chatflow, options.hideBackToInboxButton ??
|
|
30
|
+
return NativeHubspotWrapper.openChat(chatflow, options.hideBackToInboxButton ?? false);
|
|
28
31
|
},
|
|
29
32
|
|
|
30
33
|
setIdentity(params: SetIdentityParams): Promise<void> {
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
package com.marcinolek.reactnativehubspotwrapper
|
|
2
|
-
|
|
3
|
-
import android.app.Activity
|
|
4
|
-
import android.app.Application
|
|
5
|
-
import android.os.Bundle
|
|
6
|
-
import android.os.Handler
|
|
7
|
-
import android.os.Looper
|
|
8
|
-
import android.util.Log
|
|
9
|
-
import android.view.View
|
|
10
|
-
import android.view.ViewGroup
|
|
11
|
-
import android.webkit.WebView
|
|
12
|
-
import java.util.WeakHashMap
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Hides the HubSpot chat widget's "back to inbox" / conversations-list navigation
|
|
16
|
-
* inside [HubspotWebActivity] (which lives in HubSpot's compiled `.aar` and which
|
|
17
|
-
* we therefore can't modify directly).
|
|
18
|
-
*
|
|
19
|
-
* Approach:
|
|
20
|
-
*
|
|
21
|
-
* 1. We register a single [Application.ActivityLifecycleCallbacks] from the wrapper
|
|
22
|
-
* module's init.
|
|
23
|
-
* 2. Each time `com.hubspot.mobilesdk.HubspotWebActivity` resumes, we depth-first
|
|
24
|
-
* walk its decor view to find the `WebView`.
|
|
25
|
-
* 3. We `evaluateJavascript` a heuristic-based hider that walks all same-origin
|
|
26
|
-
* sub-frames (the chat UI itself lives in an iframe on `app-na2.hubspot.com`)
|
|
27
|
-
* and hides any `<button>`/`<a>`/`role="button"` whose accessible label,
|
|
28
|
-
* `data-test-id`, title, id, class or short visible text normalizes (camelCase
|
|
29
|
-
* -> spaces, `_-` -> spaces, lowercase) to something matching
|
|
30
|
-
* `\b(back|inbox|previous|conversations)\b`. A `MutationObserver` is then
|
|
31
|
-
* attached so late-mounted buttons get hidden too.
|
|
32
|
-
* 4. We re-fire `evaluateJavascript` periodically for a few seconds so the iframe
|
|
33
|
-
* walk catches the chat iframe as soon as it mounts (it's loaded asynchronously
|
|
34
|
-
* by HubSpot's `project.js` bundle after the activity opens).
|
|
35
|
-
*
|
|
36
|
-
* The implementation is intentionally identical in spirit to the iOS hider in
|
|
37
|
-
* `HubspotWrapperImpl.swift` so the same JS works on both platforms.
|
|
38
|
-
*/
|
|
39
|
-
internal object HubspotBackButtonHider {
|
|
40
|
-
private const val TAG = "HubspotWrapper"
|
|
41
|
-
|
|
42
|
-
const val EXTRA_HIDE_BACK_TO_INBOX_BUTTON =
|
|
43
|
-
"com.marcinolek.reactnativehubspotwrapper.HIDE_BACK_TO_INBOX_BUTTON"
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The fully-qualified name of HubSpot's chat activity in the AAR. Matched by
|
|
47
|
-
* string so we don't have to import / depend on it for compile-time resolution.
|
|
48
|
-
*/
|
|
49
|
-
private const val WEB_ACTIVITY_NAME = "com.hubspot.mobilesdk.HubspotWebActivity"
|
|
50
|
-
|
|
51
|
-
/** How often we re-fire `evaluateJavascript` to catch late-loaded iframes. */
|
|
52
|
-
private const val RESCAN_INTERVAL_MS = 250L
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Total time we keep re-firing after the activity resumes. The chat iframe
|
|
56
|
-
* normally appears within ~2s; we give a generous margin and then stop to
|
|
57
|
-
* avoid burning CPU forever if the user leaves the chat open for a long time.
|
|
58
|
-
*/
|
|
59
|
-
private const val MAX_SCAN_DURATION_MS = 10_000L
|
|
60
|
-
|
|
61
|
-
@Volatile
|
|
62
|
-
private var installed = false
|
|
63
|
-
|
|
64
|
-
private val handler = Handler(Looper.getMainLooper())
|
|
65
|
-
private val scheduledInjections = WeakHashMap<WebView, Runnable>()
|
|
66
|
-
|
|
67
|
-
@Synchronized
|
|
68
|
-
fun installOnce(app: Application) {
|
|
69
|
-
if (installed) return
|
|
70
|
-
installed = true
|
|
71
|
-
|
|
72
|
-
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
|
|
73
|
-
override fun onActivityResumed(activity: Activity) {
|
|
74
|
-
if (activity.javaClass.name != WEB_ACTIVITY_NAME) return
|
|
75
|
-
if (!activity.intent.getBooleanExtra(EXTRA_HIDE_BACK_TO_INBOX_BUTTON, true)) {
|
|
76
|
-
cancelHiderInjection(findWebView(activity.window.decorView))
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
val webView = findWebView(activity.window.decorView)
|
|
80
|
-
if (webView == null) {
|
|
81
|
-
Log.w(TAG, "installBackButtonHider: WebView not found in $WEB_ACTIVITY_NAME view tree")
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
Log.i(TAG, "installBackButtonHider: scheduling JS injection on $webView")
|
|
85
|
-
scheduleHiderInjection(webView)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
|
89
|
-
override fun onActivityStarted(activity: Activity) {}
|
|
90
|
-
override fun onActivityPaused(activity: Activity) {
|
|
91
|
-
if (activity.javaClass.name == WEB_ACTIVITY_NAME) {
|
|
92
|
-
cancelHiderInjection(findWebView(activity.window.decorView))
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
override fun onActivityStopped(activity: Activity) {}
|
|
96
|
-
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
97
|
-
override fun onActivityDestroyed(activity: Activity) {
|
|
98
|
-
if (activity.javaClass.name == WEB_ACTIVITY_NAME) {
|
|
99
|
-
cancelHiderInjection(findWebView(activity.window.decorView))
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private fun findWebView(view: View?): WebView? {
|
|
106
|
-
if (view is WebView) return view
|
|
107
|
-
if (view is ViewGroup) {
|
|
108
|
-
for (i in 0 until view.childCount) {
|
|
109
|
-
val found = findWebView(view.getChildAt(i))
|
|
110
|
-
if (found != null) return found
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return null
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private fun scheduleHiderInjection(webView: WebView) {
|
|
117
|
-
cancelHiderInjection(webView)
|
|
118
|
-
|
|
119
|
-
val start = System.currentTimeMillis()
|
|
120
|
-
val runnable = object : Runnable {
|
|
121
|
-
override fun run() {
|
|
122
|
-
try {
|
|
123
|
-
webView.evaluateJavascript(BACK_BUTTON_HIDER_JS, null)
|
|
124
|
-
} catch (error: Exception) {
|
|
125
|
-
Log.w(TAG, "evaluateJavascript failed", error)
|
|
126
|
-
}
|
|
127
|
-
if (System.currentTimeMillis() - start < MAX_SCAN_DURATION_MS) {
|
|
128
|
-
handler.postDelayed(this, RESCAN_INTERVAL_MS)
|
|
129
|
-
} else {
|
|
130
|
-
scheduledInjections.remove(webView)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
scheduledInjections[webView] = runnable
|
|
135
|
-
handler.post(runnable)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private fun cancelHiderInjection(webView: WebView?) {
|
|
139
|
-
if (webView == null) return
|
|
140
|
-
val runnable = scheduledInjections.remove(webView) ?: return
|
|
141
|
-
handler.removeCallbacks(runnable)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Heuristic-based hider for the chat widget's back/inbox button. Identical in
|
|
146
|
-
* shape to the iOS version in `HubspotWrapperImpl.swift`. Designed to be safe
|
|
147
|
-
* to invoke many times: a `__rnHubspotHiderInstalled` per-document flag short-
|
|
148
|
-
* circuits re-installation, while the periodic re-fire from
|
|
149
|
-
* [scheduleHiderInjection] still allows newly-mounted iframes to be picked up.
|
|
150
|
-
*/
|
|
151
|
-
private const val BACK_BUTTON_HIDER_JS = """
|
|
152
|
-
(function() {
|
|
153
|
-
function bootstrap(win) {
|
|
154
|
-
var doc;
|
|
155
|
-
try { doc = win.document; } catch (_) { return; }
|
|
156
|
-
if (!doc || doc.__rnHubspotHiderInstalled) return;
|
|
157
|
-
doc.__rnHubspotHiderInstalled = true;
|
|
158
|
-
|
|
159
|
-
var BACK_PATTERN = /\b(back|inbox|previous|conversations)\b/i;
|
|
160
|
-
|
|
161
|
-
function normalize(s) {
|
|
162
|
-
if (!s) return '';
|
|
163
|
-
return String(s)
|
|
164
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
165
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
166
|
-
.replace(/[_-]+/g, ' ')
|
|
167
|
-
.toLowerCase();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function looksLikeBackControl(el) {
|
|
171
|
-
if (!el || el.nodeType !== 1) return false;
|
|
172
|
-
if (el.getAttribute('data-rn-hubspot-wrapper-hidden') === '1') return false;
|
|
173
|
-
var tag = el.tagName;
|
|
174
|
-
if (tag !== 'BUTTON' && tag !== 'A' && el.getAttribute('role') !== 'button') return false;
|
|
175
|
-
var attrs = [
|
|
176
|
-
el.getAttribute('aria-label'),
|
|
177
|
-
el.getAttribute('data-test-id'),
|
|
178
|
-
el.getAttribute('data-test'),
|
|
179
|
-
el.getAttribute('title'),
|
|
180
|
-
el.getAttribute('id'),
|
|
181
|
-
el.getAttribute('class')
|
|
182
|
-
];
|
|
183
|
-
for (var i = 0; i < attrs.length; i++) {
|
|
184
|
-
if (attrs[i] && BACK_PATTERN.test(normalize(attrs[i]))) return true;
|
|
185
|
-
}
|
|
186
|
-
var text = (el.textContent || '').trim();
|
|
187
|
-
if (text && text.length <= 32 && BACK_PATTERN.test(normalize(text))) return true;
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function hide(el) {
|
|
192
|
-
try {
|
|
193
|
-
el.setAttribute('data-rn-hubspot-wrapper-hidden', '1');
|
|
194
|
-
el.style.setProperty('display', 'none', 'important');
|
|
195
|
-
el.style.setProperty('visibility', 'hidden', 'important');
|
|
196
|
-
el.style.setProperty('pointer-events', 'none', 'important');
|
|
197
|
-
} catch (_) {}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Hide `el`, then walk up to a few ancestors and also hide any wrapper
|
|
201
|
-
// that becomes empty (no remaining unhidden element children) as a
|
|
202
|
-
// result. HubSpot's chat header has a back-button slot whose padding
|
|
203
|
-
// / min-width / flex-basis stays around even when the inner button is
|
|
204
|
-
// `display:none`, which shows up as the avatar shifting right after the
|
|
205
|
-
// first message is sent. Capping at depth 3 keeps us inside the header.
|
|
206
|
-
function hideUpwards(el) {
|
|
207
|
-
hide(el);
|
|
208
|
-
var current = el;
|
|
209
|
-
for (var depth = 0; depth < 3; depth++) {
|
|
210
|
-
var parent = current.parentElement;
|
|
211
|
-
if (!parent) break;
|
|
212
|
-
if (parent === doc.documentElement || parent === doc.body) break;
|
|
213
|
-
var visibleCount = 0;
|
|
214
|
-
var children = parent.children;
|
|
215
|
-
for (var i = 0; i < children.length; i++) {
|
|
216
|
-
if (children[i].getAttribute('data-rn-hubspot-wrapper-hidden') !== '1') {
|
|
217
|
-
visibleCount++;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (visibleCount > 0) break;
|
|
221
|
-
hide(parent);
|
|
222
|
-
current = parent;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
var observed = new WeakSet();
|
|
227
|
-
function scanRoot(root) {
|
|
228
|
-
if (!root) return;
|
|
229
|
-
try {
|
|
230
|
-
var nodes = root.querySelectorAll('button, a, [role="button"]');
|
|
231
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
232
|
-
if (looksLikeBackControl(nodes[i])) hideUpwards(nodes[i]);
|
|
233
|
-
}
|
|
234
|
-
var all = root.querySelectorAll('*');
|
|
235
|
-
for (var j = 0; j < all.length; j++) {
|
|
236
|
-
var sr = all[j].shadowRoot;
|
|
237
|
-
if (sr) {
|
|
238
|
-
scanRoot(sr);
|
|
239
|
-
attachObserver(sr);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
} catch (_) {}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function attachObserver(root) {
|
|
246
|
-
if (!root || observed.has(root)) return;
|
|
247
|
-
observed.add(root);
|
|
248
|
-
try {
|
|
249
|
-
var mo = new MutationObserver(function() { scanRoot(root); });
|
|
250
|
-
mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-label', 'data-test-id', 'data-test', 'title', 'class'] });
|
|
251
|
-
} catch (_) {}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function go() {
|
|
255
|
-
scanRoot(doc);
|
|
256
|
-
attachObserver(doc.documentElement || doc);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (doc.readyState === 'loading') {
|
|
260
|
-
doc.addEventListener('DOMContentLoaded', go, { once: true });
|
|
261
|
-
} else {
|
|
262
|
-
go();
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function walkAllFrames(win) {
|
|
267
|
-
if (!win) return;
|
|
268
|
-
bootstrap(win);
|
|
269
|
-
try {
|
|
270
|
-
for (var i = 0; i < win.frames.length; i++) {
|
|
271
|
-
walkAllFrames(win.frames[i]);
|
|
272
|
-
}
|
|
273
|
-
} catch (_) {}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
walkAllFrames(window);
|
|
277
|
-
})();
|
|
278
|
-
"""
|
|
279
|
-
}
|