react-native-hubspot-wrapper 0.4.0 → 0.5.0
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 +19 -2
- package/android/src/main/java/com/reactnativehubspotwrapper/HubspotChatScriptInstaller.kt +232 -0
- package/android/src/main/java/com/reactnativehubspotwrapper/HubspotWrapperModule.kt +7 -6
- package/ios/HubspotMobileSDK/Views/ChatView/HubspotChatView.swift +0 -3
- package/ios/HubspotWrapperImpl.swift +5 -197
- package/ios/RNHubspotWrapper.mm +2 -2
- package/package.json +3 -2
- package/scripts/update-hubspot-ios-sdk.sh +0 -24
- package/src/index.ts +9 -2
- package/src/specs/NativeHubspotWrapper.ts +1 -1
- package/android/.gradle/8.13/checksums/checksums.lock +0 -0
- package/android/.gradle/8.13/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.13/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.13/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/src/main/java/com/reactnativehubspotwrapper/HubspotBackButtonHider.kt +0 -250
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Early release (`0.x`). The public API may evolve before `1.0`.
|
|
|
12
12
|
Currently supported:
|
|
13
13
|
|
|
14
14
|
- `initialize()`
|
|
15
|
-
- `openChat(chatflow)`
|
|
15
|
+
- `openChat(chatflow, options?)`
|
|
16
16
|
- `setIdentity({ identityToken, email? })`
|
|
17
17
|
- `setProperties(properties)`
|
|
18
18
|
- `clearUserData()`
|
|
@@ -61,14 +61,31 @@ await HubspotWrapper.setProperties([{ name: 'plan', value: 'pro' }]);
|
|
|
61
61
|
await HubspotWrapper.openChat('support');
|
|
62
62
|
```
|
|
63
63
|
|
|
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.
|
|
67
|
+
|
|
64
68
|
## API
|
|
65
69
|
|
|
66
70
|
- `initialize(): Promise<void>`
|
|
67
|
-
- `openChat(chatflow: string): Promise<void>`
|
|
71
|
+
- `openChat(chatflow: string, options?: { hideBackToInboxButton?: boolean }): Promise<void>`
|
|
68
72
|
- `setIdentity({ identityToken, email? }): Promise<void>`
|
|
69
73
|
- `setProperties(properties): Promise<void>`
|
|
70
74
|
- `clearUserData(): Promise<void>`
|
|
71
75
|
|
|
76
|
+
### `openChat(chatflow, options?)`
|
|
77
|
+
|
|
78
|
+
Opens the HubSpot chat UI for the provided chatflow.
|
|
79
|
+
|
|
80
|
+
- `hideBackToInboxButton` is deprecated and ignored.
|
|
81
|
+
|
|
82
|
+
### `clearUserData()`
|
|
83
|
+
|
|
84
|
+
Clears the native SDK's current user/session state and waits for HubSpot chat
|
|
85
|
+
visitor cookies/data cleanup to finish before resolving. This is intended for
|
|
86
|
+
logout or account-switch flows so the next chat session does not reuse the
|
|
87
|
+
previous visitor identity.
|
|
88
|
+
|
|
72
89
|
## iOS SDK source strategy
|
|
73
90
|
|
|
74
91
|
This package vendors HubSpot iOS SDK source files under `ios/HubspotMobileSDK`.
|
|
@@ -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,7 +42,8 @@ class HubspotWrapperModule(reactContext: ReactApplicationContext) :
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
@Suppress("UNUSED_PARAMETER")
|
|
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)
|
|
@@ -56,7 +57,7 @@ class HubspotWrapperModule(reactContext: ReactApplicationContext) :
|
|
|
56
57
|
|
|
57
58
|
override fun setIdentity(identityToken: String, email: String?, promise: Promise) {
|
|
58
59
|
try {
|
|
59
|
-
hubspotManager.setUserIdentity(
|
|
60
|
+
hubspotManager.setUserIdentity(email ?: "", identityToken)
|
|
60
61
|
promise.resolve(null)
|
|
61
62
|
} catch (error: Exception) {
|
|
62
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,25 +5,6 @@ import WebKit
|
|
|
5
5
|
|
|
6
6
|
@objcMembers
|
|
7
7
|
public class HubspotWrapperImpl: NSObject {
|
|
8
|
-
/// Substrings used to match `WKWebsiteDataRecord.displayName` (which is the host /
|
|
9
|
-
/// eTLD+1, e.g. `hubspot.com`, `hs-banner.com`, `hsadspixel.net`) for the various
|
|
10
|
-
/// domains the chat widget loads from. Anything matching is wiped on `clearUserData`.
|
|
11
|
-
///
|
|
12
|
-
/// We deliberately use substrings rather than exact hosts because the widget pulls
|
|
13
|
-
/// resources from a long tail of HubSpot CDNs - `js.hs-banner.com`, `js.hubspot.com`,
|
|
14
|
-
/// `static.hsappstatic.net`, `app-na2.hubspot.com`, `*.hubspotqa.com`, etc. - and any
|
|
15
|
-
/// of those can hold cookies / localStorage / IndexedDB that re-attach a returning
|
|
16
|
-
/// visitor or surface the unsent draft message on the next open.
|
|
17
|
-
private static let chatDataDomainMatches: [String] = [
|
|
18
|
-
"hubspot",
|
|
19
|
-
"hsforms",
|
|
20
|
-
"hs-banner",
|
|
21
|
-
"hs-scripts",
|
|
22
|
-
"hsadspixel",
|
|
23
|
-
"hsappstatic",
|
|
24
|
-
"usemessages",
|
|
25
|
-
]
|
|
26
|
-
|
|
27
8
|
public func initialize(_ outError: NSErrorPointer) -> Bool {
|
|
28
9
|
let semaphore = DispatchSemaphore(value: 0)
|
|
29
10
|
var configureError: NSError?
|
|
@@ -46,7 +27,7 @@ public class HubspotWrapperImpl: NSObject {
|
|
|
46
27
|
return true
|
|
47
28
|
}
|
|
48
29
|
|
|
49
|
-
public func openChat(_ chatflow: String, error outError: NSErrorPointer) -> Bool {
|
|
30
|
+
public func openChat(_ chatflow: String, hideBackToInboxButton _: Bool, error outError: NSErrorPointer) -> Bool {
|
|
50
31
|
var didSucceed = false
|
|
51
32
|
let presentBlock = {
|
|
52
33
|
guard let rootVC = Self.topViewController() else {
|
|
@@ -120,9 +101,9 @@ public class HubspotWrapperImpl: NSObject {
|
|
|
120
101
|
/// `sessionStorage` / `IndexedDB`. Cookie-only clearing leaves the draft visible
|
|
121
102
|
/// on the next open, which is exactly the bug we kept seeing.
|
|
122
103
|
///
|
|
123
|
-
/// We therefore
|
|
124
|
-
///
|
|
125
|
-
///
|
|
104
|
+
/// We therefore remove all shared WebKit website data during logout. This is intentionally
|
|
105
|
+
/// broader than HubSpot-only cleanup: fetching individual `WKWebsiteDataRecord`s crashed
|
|
106
|
+
/// in production inside WebKit's `_fetchDataRecords...allDataStores` path.
|
|
126
107
|
public func clearUserData(_ completion: @escaping () -> Void) {
|
|
127
108
|
Task { @MainActor in
|
|
128
109
|
HubspotManager.shared.clearUserData()
|
|
@@ -134,182 +115,9 @@ public class HubspotWrapperImpl: NSObject {
|
|
|
134
115
|
private static func deleteAllChatWebsiteData() async {
|
|
135
116
|
let store = WKWebsiteDataStore.default()
|
|
136
117
|
let allTypes = WKWebsiteDataStore.allWebsiteDataTypes()
|
|
137
|
-
|
|
138
|
-
let matching = records.filter { record in
|
|
139
|
-
let host = record.displayName.lowercased()
|
|
140
|
-
return chatDataDomainMatches.contains(where: host.contains)
|
|
141
|
-
}
|
|
142
|
-
guard !matching.isEmpty else { return }
|
|
143
|
-
await store.removeData(ofTypes: allTypes, for: matching)
|
|
118
|
+
await store.removeData(ofTypes: allTypes, modifiedSince: .distantPast)
|
|
144
119
|
}
|
|
145
120
|
|
|
146
|
-
/// Install a `WKUserScript` on `controller` that hides any "back to inbox" /
|
|
147
|
-
/// conversations-list navigation rendered by the HubSpot chat widget.
|
|
148
|
-
///
|
|
149
|
-
/// Called from the patched `HubspotChatWebView.WebviewCoordinator.setupScripts()`
|
|
150
|
-
/// (see `update-hubspot-ios-sdk.sh` for the patch that re-applies that hook on
|
|
151
|
-
/// every SDK sync). The actual heuristic-based hide JS lives here in our own
|
|
152
|
-
/// non-vendored file so SDK updates don't churn the implementation.
|
|
153
|
-
///
|
|
154
|
-
/// We can't rely on a single CSS selector because:
|
|
155
|
-
/// - HubSpot's widget uses minified/hashed class names that change across
|
|
156
|
-
/// releases.
|
|
157
|
-
/// - Parts of the widget render inside shadow roots, which a top-level
|
|
158
|
-
/// `<style>` doesn't pierce.
|
|
159
|
-
/// - The button can mount after first paint as the widget hydrates.
|
|
160
|
-
///
|
|
161
|
-
/// The injected script installs a `MutationObserver` that walks the full tree
|
|
162
|
-
/// (recursing into open shadow roots) and hides anything that *behaves* like a
|
|
163
|
-
/// back button - matched on `aria-label`, `data-test-id`, `title`, `id`, `class`,
|
|
164
|
-
/// and visible text. Re-scans periodically for the first few seconds to cover
|
|
165
|
-
/// late-mounted shadow roots. Injected at document start in every frame.
|
|
166
|
-
@objc public static func installBackButtonHider(on controller: WKUserContentController) {
|
|
167
|
-
let script = WKUserScript(
|
|
168
|
-
source: backButtonHiderJS,
|
|
169
|
-
injectionTime: .atDocumentStart,
|
|
170
|
-
forMainFrameOnly: false
|
|
171
|
-
)
|
|
172
|
-
controller.addUserScript(script)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/// Heuristic-based hider for the chat widget's back/inbox button. Designed to
|
|
176
|
-
/// work both as a `WKUserScript` (per-frame at `.atDocumentStart`) and as a
|
|
177
|
-
/// one-shot `evaluateJavascript` payload from the Android side - hence the
|
|
178
|
-
/// `walkAllFrames` / `bootstrap` split with the per-document install guard.
|
|
179
|
-
/// Keep this string in lockstep with `HubspotBackButtonHider.BACK_BUTTON_HIDER_JS`
|
|
180
|
-
/// in the Android wrapper. Insert spaces at camelCase boundaries and collapse
|
|
181
|
-
/// underscores/dashes to spaces so word-boundary matching catches class names
|
|
182
|
-
/// like `ConversationView__BackButton`. (In JS regex, `_` is a word char, so
|
|
183
|
-
/// without this `\\bback\\b` would fail against `..._back_...`.)
|
|
184
|
-
private static let backButtonHiderJS: String = """
|
|
185
|
-
(function() {
|
|
186
|
-
function bootstrap(win) {
|
|
187
|
-
var doc;
|
|
188
|
-
try { doc = win.document; } catch (_) { return; }
|
|
189
|
-
if (!doc || doc.__rnHubspotHiderInstalled) return;
|
|
190
|
-
doc.__rnHubspotHiderInstalled = true;
|
|
191
|
-
|
|
192
|
-
var BACK_PATTERN = /\\b(back|inbox|previous|conversations)\\b/i;
|
|
193
|
-
|
|
194
|
-
function normalize(s) {
|
|
195
|
-
if (!s) return '';
|
|
196
|
-
return String(s)
|
|
197
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
198
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
199
|
-
.replace(/[_-]+/g, ' ')
|
|
200
|
-
.toLowerCase();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function looksLikeBackControl(el) {
|
|
204
|
-
if (!el || el.nodeType !== 1) return false;
|
|
205
|
-
if (el.getAttribute('data-rn-hubspot-wrapper-hidden') === '1') return false;
|
|
206
|
-
var tag = el.tagName;
|
|
207
|
-
if (tag !== 'BUTTON' && tag !== 'A' && el.getAttribute('role') !== 'button') return false;
|
|
208
|
-
var attrs = [
|
|
209
|
-
el.getAttribute('aria-label'),
|
|
210
|
-
el.getAttribute('data-test-id'),
|
|
211
|
-
el.getAttribute('data-test'),
|
|
212
|
-
el.getAttribute('title'),
|
|
213
|
-
el.getAttribute('id'),
|
|
214
|
-
el.getAttribute('class')
|
|
215
|
-
];
|
|
216
|
-
for (var i = 0; i < attrs.length; i++) {
|
|
217
|
-
if (attrs[i] && BACK_PATTERN.test(normalize(attrs[i]))) return true;
|
|
218
|
-
}
|
|
219
|
-
var text = (el.textContent || '').trim();
|
|
220
|
-
if (text && text.length <= 32 && BACK_PATTERN.test(normalize(text))) return true;
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function hide(el) {
|
|
225
|
-
try {
|
|
226
|
-
el.setAttribute('data-rn-hubspot-wrapper-hidden', '1');
|
|
227
|
-
el.style.setProperty('display', 'none', 'important');
|
|
228
|
-
el.style.setProperty('visibility', 'hidden', 'important');
|
|
229
|
-
el.style.setProperty('pointer-events', 'none', 'important');
|
|
230
|
-
} catch (_) {}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Hide `el`, then walk up to a few ancestors and also hide any wrapper
|
|
234
|
-
// that becomes empty (no remaining unhidden element children) as a
|
|
235
|
-
// result. HubSpot's chat header has a back-button slot whose padding
|
|
236
|
-
// / min-width / flex-basis stays around even when the inner button is
|
|
237
|
-
// `display:none`, which shows up as the avatar shifting right after the
|
|
238
|
-
// first message is sent. Capping at depth 3 keeps us inside the header.
|
|
239
|
-
function hideUpwards(el) {
|
|
240
|
-
hide(el);
|
|
241
|
-
var current = el;
|
|
242
|
-
for (var depth = 0; depth < 3; depth++) {
|
|
243
|
-
var parent = current.parentElement;
|
|
244
|
-
if (!parent) break;
|
|
245
|
-
if (parent === doc.documentElement || parent === doc.body) break;
|
|
246
|
-
var visibleCount = 0;
|
|
247
|
-
var children = parent.children;
|
|
248
|
-
for (var i = 0; i < children.length; i++) {
|
|
249
|
-
if (children[i].getAttribute('data-rn-hubspot-wrapper-hidden') !== '1') {
|
|
250
|
-
visibleCount++;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
if (visibleCount > 0) break;
|
|
254
|
-
hide(parent);
|
|
255
|
-
current = parent;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
var observed = new WeakSet();
|
|
260
|
-
function scanRoot(root) {
|
|
261
|
-
if (!root) return;
|
|
262
|
-
try {
|
|
263
|
-
var nodes = root.querySelectorAll('button, a, [role="button"]');
|
|
264
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
265
|
-
if (looksLikeBackControl(nodes[i])) hideUpwards(nodes[i]);
|
|
266
|
-
}
|
|
267
|
-
var all = root.querySelectorAll('*');
|
|
268
|
-
for (var j = 0; j < all.length; j++) {
|
|
269
|
-
var sr = all[j].shadowRoot;
|
|
270
|
-
if (sr) {
|
|
271
|
-
scanRoot(sr);
|
|
272
|
-
attachObserver(sr);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
} catch (_) {}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function attachObserver(root) {
|
|
279
|
-
if (!root || observed.has(root)) return;
|
|
280
|
-
observed.add(root);
|
|
281
|
-
try {
|
|
282
|
-
var mo = new MutationObserver(function() { scanRoot(root); });
|
|
283
|
-
mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-label', 'data-test-id', 'data-test', 'title', 'class'] });
|
|
284
|
-
} catch (_) {}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function go() {
|
|
288
|
-
scanRoot(doc);
|
|
289
|
-
attachObserver(doc.documentElement || doc);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (doc.readyState === 'loading') {
|
|
293
|
-
doc.addEventListener('DOMContentLoaded', go, { once: true });
|
|
294
|
-
} else {
|
|
295
|
-
go();
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function walkAllFrames(win) {
|
|
300
|
-
if (!win) return;
|
|
301
|
-
bootstrap(win);
|
|
302
|
-
try {
|
|
303
|
-
for (var i = 0; i < win.frames.length; i++) {
|
|
304
|
-
walkAllFrames(win.frames[i]);
|
|
305
|
-
}
|
|
306
|
-
} catch (_) {}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
walkAllFrames(window);
|
|
310
|
-
})();
|
|
311
|
-
"""
|
|
312
|
-
|
|
313
121
|
private static func topViewController(
|
|
314
122
|
base: UIViewController? = UIApplication.shared.connectedScenes
|
|
315
123
|
.compactMap { $0 as? UIWindowScene }
|
package/ios/RNHubspotWrapper.mm
CHANGED
|
@@ -34,10 +34,10 @@ RCT_EXPORT_MODULE(NativeHubspotWrapper)
|
|
|
34
34
|
reject(@"INIT_ERROR", @"Failed to initialize HubSpot SDK", error);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
- (void)openChat:(NSString *)chatflow resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
37
|
+
- (void)openChat:(NSString *)chatflow hideBackToInboxButton:(BOOL)hideBackToInboxButton resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
38
38
|
{
|
|
39
39
|
NSError *error = nil;
|
|
40
|
-
BOOL didOpen = [_impl openChat:chatflow error:&error];
|
|
40
|
+
BOOL didOpen = [_impl openChat:chatflow hideBackToInboxButton:hideBackToInboxButton error:&error];
|
|
41
41
|
if (didOpen) {
|
|
42
42
|
resolve(nil);
|
|
43
43
|
return;
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-hubspot-wrapper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "TurboModule wrapper for HubSpot mobile chat SDK",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"src",
|
|
9
|
-
"android",
|
|
9
|
+
"android/build.gradle",
|
|
10
|
+
"android/src",
|
|
10
11
|
"ios",
|
|
11
12
|
"ReactNativeHubspotWrapper.podspec",
|
|
12
13
|
"react-native.config.js",
|
|
@@ -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
|
@@ -7,6 +7,13 @@ export type SetIdentityParams = {
|
|
|
7
7
|
email?: string | null;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export type OpenChatOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated No-op. HubSpot's conversations navigation remains visible.
|
|
13
|
+
*/
|
|
14
|
+
hideBackToInboxButton?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
10
17
|
function ensureNonEmpty(value: string, fieldName: string): void {
|
|
11
18
|
if (!value || !value.trim()) {
|
|
12
19
|
throw new Error(`\`${fieldName}\` must be a non-empty string.`);
|
|
@@ -18,9 +25,9 @@ const HubspotWrapper = {
|
|
|
18
25
|
return NativeHubspotWrapper.initialize();
|
|
19
26
|
},
|
|
20
27
|
|
|
21
|
-
openChat(chatflow: string): Promise<void> {
|
|
28
|
+
openChat(chatflow: string, options: OpenChatOptions = {}): Promise<void> {
|
|
22
29
|
ensureNonEmpty(chatflow, 'chatflow');
|
|
23
|
-
return NativeHubspotWrapper.openChat(chatflow);
|
|
30
|
+
return NativeHubspotWrapper.openChat(chatflow, options.hideBackToInboxButton ?? false);
|
|
24
31
|
},
|
|
25
32
|
|
|
26
33
|
setIdentity(params: SetIdentityParams): Promise<void> {
|
|
@@ -8,7 +8,7 @@ export type HubspotProperty = {
|
|
|
8
8
|
|
|
9
9
|
export interface Spec extends TurboModule {
|
|
10
10
|
initialize(): Promise<void>;
|
|
11
|
-
openChat(chatflow: string): Promise<void>;
|
|
11
|
+
openChat(chatflow: string, hideBackToInboxButton: boolean): Promise<void>;
|
|
12
12
|
setIdentity(identityToken: string, email: string | null): Promise<void>;
|
|
13
13
|
setProperties(properties: ReadonlyArray<HubspotProperty>): Promise<void>;
|
|
14
14
|
clearUserData(): Promise<void>;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
@@ -1,250 +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
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Hides the HubSpot chat widget's "back to inbox" / conversations-list navigation
|
|
15
|
-
* inside [HubspotWebActivity] (which lives in HubSpot's compiled `.aar` and which
|
|
16
|
-
* we therefore can't modify directly).
|
|
17
|
-
*
|
|
18
|
-
* Approach:
|
|
19
|
-
*
|
|
20
|
-
* 1. We register a single [Application.ActivityLifecycleCallbacks] from the wrapper
|
|
21
|
-
* module's init.
|
|
22
|
-
* 2. Each time `com.hubspot.mobilesdk.HubspotWebActivity` resumes, we depth-first
|
|
23
|
-
* walk its decor view to find the `WebView`.
|
|
24
|
-
* 3. We `evaluateJavascript` a heuristic-based hider that walks all same-origin
|
|
25
|
-
* sub-frames (the chat UI itself lives in an iframe on `app-na2.hubspot.com`)
|
|
26
|
-
* and hides any `<button>`/`<a>`/`role="button"` whose accessible label,
|
|
27
|
-
* `data-test-id`, title, id, class or short visible text normalizes (camelCase
|
|
28
|
-
* -> spaces, `_-` -> spaces, lowercase) to something matching
|
|
29
|
-
* `\b(back|inbox|previous|conversations)\b`. A `MutationObserver` is then
|
|
30
|
-
* attached so late-mounted buttons get hidden too.
|
|
31
|
-
* 4. We re-fire `evaluateJavascript` periodically for a few seconds so the iframe
|
|
32
|
-
* walk catches the chat iframe as soon as it mounts (it's loaded asynchronously
|
|
33
|
-
* by HubSpot's `project.js` bundle after the activity opens).
|
|
34
|
-
*
|
|
35
|
-
* The implementation is intentionally identical in spirit to the iOS hider in
|
|
36
|
-
* `HubspotWrapperImpl.swift` so the same JS works on both platforms.
|
|
37
|
-
*/
|
|
38
|
-
internal object HubspotBackButtonHider {
|
|
39
|
-
private const val TAG = "HubspotWrapper"
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The fully-qualified name of HubSpot's chat activity in the AAR. Matched by
|
|
43
|
-
* string so we don't have to import / depend on it for compile-time resolution.
|
|
44
|
-
*/
|
|
45
|
-
private const val WEB_ACTIVITY_NAME = "com.hubspot.mobilesdk.HubspotWebActivity"
|
|
46
|
-
|
|
47
|
-
/** How often we re-fire `evaluateJavascript` to catch late-loaded iframes. */
|
|
48
|
-
private const val RESCAN_INTERVAL_MS = 250L
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Total time we keep re-firing after the activity resumes. The chat iframe
|
|
52
|
-
* normally appears within ~2s; we give a generous margin and then stop to
|
|
53
|
-
* avoid burning CPU forever if the user leaves the chat open for a long time.
|
|
54
|
-
*/
|
|
55
|
-
private const val MAX_SCAN_DURATION_MS = 10_000L
|
|
56
|
-
|
|
57
|
-
@Volatile
|
|
58
|
-
private var installed = false
|
|
59
|
-
|
|
60
|
-
@Synchronized
|
|
61
|
-
fun installOnce(app: Application) {
|
|
62
|
-
if (installed) return
|
|
63
|
-
installed = true
|
|
64
|
-
|
|
65
|
-
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
|
|
66
|
-
override fun onActivityResumed(activity: Activity) {
|
|
67
|
-
if (activity.javaClass.name != WEB_ACTIVITY_NAME) return
|
|
68
|
-
val webView = findWebView(activity.window.decorView)
|
|
69
|
-
if (webView == null) {
|
|
70
|
-
Log.w(TAG, "installBackButtonHider: WebView not found in $WEB_ACTIVITY_NAME view tree")
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
Log.i(TAG, "installBackButtonHider: scheduling JS injection on $webView")
|
|
74
|
-
scheduleHiderInjection(webView)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
|
78
|
-
override fun onActivityStarted(activity: Activity) {}
|
|
79
|
-
override fun onActivityPaused(activity: Activity) {}
|
|
80
|
-
override fun onActivityStopped(activity: Activity) {}
|
|
81
|
-
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
82
|
-
override fun onActivityDestroyed(activity: Activity) {}
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private fun findWebView(view: View?): WebView? {
|
|
87
|
-
if (view is WebView) return view
|
|
88
|
-
if (view is ViewGroup) {
|
|
89
|
-
for (i in 0 until view.childCount) {
|
|
90
|
-
val found = findWebView(view.getChildAt(i))
|
|
91
|
-
if (found != null) return found
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private fun scheduleHiderInjection(webView: WebView) {
|
|
98
|
-
val handler = Handler(Looper.getMainLooper())
|
|
99
|
-
val start = System.currentTimeMillis()
|
|
100
|
-
val runnable = object : Runnable {
|
|
101
|
-
override fun run() {
|
|
102
|
-
try {
|
|
103
|
-
webView.evaluateJavascript(BACK_BUTTON_HIDER_JS, null)
|
|
104
|
-
} catch (error: Exception) {
|
|
105
|
-
Log.w(TAG, "evaluateJavascript failed", error)
|
|
106
|
-
}
|
|
107
|
-
if (System.currentTimeMillis() - start < MAX_SCAN_DURATION_MS) {
|
|
108
|
-
handler.postDelayed(this, RESCAN_INTERVAL_MS)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
handler.post(runnable)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Heuristic-based hider for the chat widget's back/inbox button. Identical in
|
|
117
|
-
* shape to the iOS version in `HubspotWrapperImpl.swift`. Designed to be safe
|
|
118
|
-
* to invoke many times: a `__rnHubspotHiderInstalled` per-document flag short-
|
|
119
|
-
* circuits re-installation, while the periodic re-fire from
|
|
120
|
-
* [scheduleHiderInjection] still allows newly-mounted iframes to be picked up.
|
|
121
|
-
*/
|
|
122
|
-
private const val BACK_BUTTON_HIDER_JS = """
|
|
123
|
-
(function() {
|
|
124
|
-
function bootstrap(win) {
|
|
125
|
-
var doc;
|
|
126
|
-
try { doc = win.document; } catch (_) { return; }
|
|
127
|
-
if (!doc || doc.__rnHubspotHiderInstalled) return;
|
|
128
|
-
doc.__rnHubspotHiderInstalled = true;
|
|
129
|
-
|
|
130
|
-
var BACK_PATTERN = /\b(back|inbox|previous|conversations)\b/i;
|
|
131
|
-
|
|
132
|
-
function normalize(s) {
|
|
133
|
-
if (!s) return '';
|
|
134
|
-
return String(s)
|
|
135
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
136
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
137
|
-
.replace(/[_-]+/g, ' ')
|
|
138
|
-
.toLowerCase();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function looksLikeBackControl(el) {
|
|
142
|
-
if (!el || el.nodeType !== 1) return false;
|
|
143
|
-
if (el.getAttribute('data-rn-hubspot-wrapper-hidden') === '1') return false;
|
|
144
|
-
var tag = el.tagName;
|
|
145
|
-
if (tag !== 'BUTTON' && tag !== 'A' && el.getAttribute('role') !== 'button') return false;
|
|
146
|
-
var attrs = [
|
|
147
|
-
el.getAttribute('aria-label'),
|
|
148
|
-
el.getAttribute('data-test-id'),
|
|
149
|
-
el.getAttribute('data-test'),
|
|
150
|
-
el.getAttribute('title'),
|
|
151
|
-
el.getAttribute('id'),
|
|
152
|
-
el.getAttribute('class')
|
|
153
|
-
];
|
|
154
|
-
for (var i = 0; i < attrs.length; i++) {
|
|
155
|
-
if (attrs[i] && BACK_PATTERN.test(normalize(attrs[i]))) return true;
|
|
156
|
-
}
|
|
157
|
-
var text = (el.textContent || '').trim();
|
|
158
|
-
if (text && text.length <= 32 && BACK_PATTERN.test(normalize(text))) return true;
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function hide(el) {
|
|
163
|
-
try {
|
|
164
|
-
el.setAttribute('data-rn-hubspot-wrapper-hidden', '1');
|
|
165
|
-
el.style.setProperty('display', 'none', 'important');
|
|
166
|
-
el.style.setProperty('visibility', 'hidden', 'important');
|
|
167
|
-
el.style.setProperty('pointer-events', 'none', 'important');
|
|
168
|
-
} catch (_) {}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Hide `el`, then walk up to a few ancestors and also hide any wrapper
|
|
172
|
-
// that becomes empty (no remaining unhidden element children) as a
|
|
173
|
-
// result. HubSpot's chat header has a back-button slot whose padding
|
|
174
|
-
// / min-width / flex-basis stays around even when the inner button is
|
|
175
|
-
// `display:none`, which shows up as the avatar shifting right after the
|
|
176
|
-
// first message is sent. Capping at depth 3 keeps us inside the header.
|
|
177
|
-
function hideUpwards(el) {
|
|
178
|
-
hide(el);
|
|
179
|
-
var current = el;
|
|
180
|
-
for (var depth = 0; depth < 3; depth++) {
|
|
181
|
-
var parent = current.parentElement;
|
|
182
|
-
if (!parent) break;
|
|
183
|
-
if (parent === doc.documentElement || parent === doc.body) break;
|
|
184
|
-
var visibleCount = 0;
|
|
185
|
-
var children = parent.children;
|
|
186
|
-
for (var i = 0; i < children.length; i++) {
|
|
187
|
-
if (children[i].getAttribute('data-rn-hubspot-wrapper-hidden') !== '1') {
|
|
188
|
-
visibleCount++;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (visibleCount > 0) break;
|
|
192
|
-
hide(parent);
|
|
193
|
-
current = parent;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
var observed = new WeakSet();
|
|
198
|
-
function scanRoot(root) {
|
|
199
|
-
if (!root) return;
|
|
200
|
-
try {
|
|
201
|
-
var nodes = root.querySelectorAll('button, a, [role="button"]');
|
|
202
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
203
|
-
if (looksLikeBackControl(nodes[i])) hideUpwards(nodes[i]);
|
|
204
|
-
}
|
|
205
|
-
var all = root.querySelectorAll('*');
|
|
206
|
-
for (var j = 0; j < all.length; j++) {
|
|
207
|
-
var sr = all[j].shadowRoot;
|
|
208
|
-
if (sr) {
|
|
209
|
-
scanRoot(sr);
|
|
210
|
-
attachObserver(sr);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
} catch (_) {}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function attachObserver(root) {
|
|
217
|
-
if (!root || observed.has(root)) return;
|
|
218
|
-
observed.add(root);
|
|
219
|
-
try {
|
|
220
|
-
var mo = new MutationObserver(function() { scanRoot(root); });
|
|
221
|
-
mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-label', 'data-test-id', 'data-test', 'title', 'class'] });
|
|
222
|
-
} catch (_) {}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function go() {
|
|
226
|
-
scanRoot(doc);
|
|
227
|
-
attachObserver(doc.documentElement || doc);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (doc.readyState === 'loading') {
|
|
231
|
-
doc.addEventListener('DOMContentLoaded', go, { once: true });
|
|
232
|
-
} else {
|
|
233
|
-
go();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function walkAllFrames(win) {
|
|
238
|
-
if (!win) return;
|
|
239
|
-
bootstrap(win);
|
|
240
|
-
try {
|
|
241
|
-
for (var i = 0; i < win.frames.length; i++) {
|
|
242
|
-
walkAllFrames(win.frames[i]);
|
|
243
|
-
}
|
|
244
|
-
} catch (_) {}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
walkAllFrames(window);
|
|
248
|
-
})();
|
|
249
|
-
"""
|
|
250
|
-
}
|