react-native-hubspot-wrapper 0.4.0 → 0.4.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 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,39 @@ await HubspotWrapper.setProperties([{ name: 'plan', value: 'pro' }]);
61
61
  await HubspotWrapper.openChat('support');
62
62
  ```
63
63
 
64
+ By default, `openChat` hides HubSpot's "back to inbox" / conversations-list
65
+ button inside the chat widget. This wrapper is opinionated toward single-chat
66
+ support flows, but you can preserve HubSpot's stock UI per chat:
67
+
68
+ ```ts
69
+ await HubspotWrapper.openChat('support', {
70
+ hideBackToInboxButton: false,
71
+ });
72
+ ```
73
+
64
74
  ## API
65
75
 
66
76
  - `initialize(): Promise<void>`
67
- - `openChat(chatflow: string): Promise<void>`
77
+ - `openChat(chatflow: string, options?: { hideBackToInboxButton?: boolean }): Promise<void>`
68
78
  - `setIdentity({ identityToken, email? }): Promise<void>`
69
79
  - `setProperties(properties): Promise<void>`
70
80
  - `clearUserData(): Promise<void>`
71
81
 
82
+ ### `openChat(chatflow, options?)`
83
+
84
+ Opens the HubSpot chat UI for the provided chatflow.
85
+
86
+ - `hideBackToInboxButton` defaults to `true`.
87
+ - Set `hideBackToInboxButton: false` to keep HubSpot's default conversations
88
+ navigation visible.
89
+
90
+ ### `clearUserData()`
91
+
92
+ Clears the native SDK's current user/session state and waits for HubSpot chat
93
+ visitor cookies/data cleanup to finish before resolving. This is intended for
94
+ logout or account-switch flows so the next chat session does not reuse the
95
+ previous visitor identity.
96
+
72
97
  ## iOS SDK source strategy
73
98
 
74
99
  This package vendors HubSpot iOS SDK source files under `ios/HubspotMobileSDK`.
@@ -9,6 +9,7 @@ import android.util.Log
9
9
  import android.view.View
10
10
  import android.view.ViewGroup
11
11
  import android.webkit.WebView
12
+ import java.util.WeakHashMap
12
13
 
13
14
  /**
14
15
  * Hides the HubSpot chat widget's "back to inbox" / conversations-list navigation
@@ -38,6 +39,9 @@ import android.webkit.WebView
38
39
  internal object HubspotBackButtonHider {
39
40
  private const val TAG = "HubspotWrapper"
40
41
 
42
+ const val EXTRA_HIDE_BACK_TO_INBOX_BUTTON =
43
+ "com.marcinolek.reactnativehubspotwrapper.HIDE_BACK_TO_INBOX_BUTTON"
44
+
41
45
  /**
42
46
  * The fully-qualified name of HubSpot's chat activity in the AAR. Matched by
43
47
  * string so we don't have to import / depend on it for compile-time resolution.
@@ -57,6 +61,9 @@ internal object HubspotBackButtonHider {
57
61
  @Volatile
58
62
  private var installed = false
59
63
 
64
+ private val handler = Handler(Looper.getMainLooper())
65
+ private val scheduledInjections = WeakHashMap<WebView, Runnable>()
66
+
60
67
  @Synchronized
61
68
  fun installOnce(app: Application) {
62
69
  if (installed) return
@@ -65,6 +72,10 @@ internal object HubspotBackButtonHider {
65
72
  app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
66
73
  override fun onActivityResumed(activity: Activity) {
67
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
+ }
68
79
  val webView = findWebView(activity.window.decorView)
69
80
  if (webView == null) {
70
81
  Log.w(TAG, "installBackButtonHider: WebView not found in $WEB_ACTIVITY_NAME view tree")
@@ -76,10 +87,18 @@ internal object HubspotBackButtonHider {
76
87
 
77
88
  override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
78
89
  override fun onActivityStarted(activity: Activity) {}
79
- override fun onActivityPaused(activity: Activity) {}
90
+ override fun onActivityPaused(activity: Activity) {
91
+ if (activity.javaClass.name == WEB_ACTIVITY_NAME) {
92
+ cancelHiderInjection(findWebView(activity.window.decorView))
93
+ }
94
+ }
80
95
  override fun onActivityStopped(activity: Activity) {}
81
96
  override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
82
- override fun onActivityDestroyed(activity: Activity) {}
97
+ override fun onActivityDestroyed(activity: Activity) {
98
+ if (activity.javaClass.name == WEB_ACTIVITY_NAME) {
99
+ cancelHiderInjection(findWebView(activity.window.decorView))
100
+ }
101
+ }
83
102
  })
84
103
  }
85
104
 
@@ -95,7 +114,8 @@ internal object HubspotBackButtonHider {
95
114
  }
96
115
 
97
116
  private fun scheduleHiderInjection(webView: WebView) {
98
- val handler = Handler(Looper.getMainLooper())
117
+ cancelHiderInjection(webView)
118
+
99
119
  val start = System.currentTimeMillis()
100
120
  val runnable = object : Runnable {
101
121
  override fun run() {
@@ -106,12 +126,21 @@ internal object HubspotBackButtonHider {
106
126
  }
107
127
  if (System.currentTimeMillis() - start < MAX_SCAN_DURATION_MS) {
108
128
  handler.postDelayed(this, RESCAN_INTERVAL_MS)
129
+ } else {
130
+ scheduledInjections.remove(webView)
109
131
  }
110
132
  }
111
133
  }
134
+ scheduledInjections[webView] = runnable
112
135
  handler.post(runnable)
113
136
  }
114
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
+
115
144
  /**
116
145
  * Heuristic-based hider for the chat widget's back/inbox button. Identical in
117
146
  * shape to the iOS version in `HubspotWrapperImpl.swift`. Designed to be safe
@@ -42,11 +42,12 @@ class HubspotWrapperModule(reactContext: ReactApplicationContext) :
42
42
  }
43
43
  }
44
44
 
45
- override fun openChat(chatflow: String, promise: Promise) {
45
+ override fun openChat(chatflow: String, hideBackToInboxButton: Boolean, promise: Promise) {
46
46
  try {
47
47
  val intent = Intent(appContext, HubspotWebActivity::class.java)
48
48
  intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
49
49
  intent.putExtra("chatflow", chatflow)
50
+ intent.putExtra(HubspotBackButtonHider.EXTRA_HIDE_BACK_TO_INBOX_BUTTON, hideBackToInboxButton)
50
51
  appContext.startActivity(intent)
51
52
  promise.resolve(null)
52
53
  } catch (error: Exception) {
@@ -5,6 +5,8 @@ import WebKit
5
5
 
6
6
  @objcMembers
7
7
  public class HubspotWrapperImpl: NSObject {
8
+ private static var shouldHideBackToInboxButton = true
9
+
8
10
  /// Substrings used to match `WKWebsiteDataRecord.displayName` (which is the host /
9
11
  /// eTLD+1, e.g. `hubspot.com`, `hs-banner.com`, `hsadspixel.net`) for the various
10
12
  /// domains the chat widget loads from. Anything matching is wiped on `clearUserData`.
@@ -46,7 +48,7 @@ public class HubspotWrapperImpl: NSObject {
46
48
  return true
47
49
  }
48
50
 
49
- public func openChat(_ chatflow: String, error outError: NSErrorPointer) -> Bool {
51
+ public func openChat(_ chatflow: String, hideBackToInboxButton: Bool, error outError: NSErrorPointer) -> Bool {
50
52
  var didSucceed = false
51
53
  let presentBlock = {
52
54
  guard let rootVC = Self.topViewController() else {
@@ -58,6 +60,7 @@ public class HubspotWrapperImpl: NSObject {
58
60
  return
59
61
  }
60
62
 
63
+ Self.shouldHideBackToInboxButton = hideBackToInboxButton
61
64
  let chatView = HubspotChatView(chatFlow: chatflow)
62
65
  let hostingController = UIHostingController(rootView: chatView)
63
66
  rootVC.present(hostingController, animated: true)
@@ -164,6 +167,8 @@ public class HubspotWrapperImpl: NSObject {
164
167
  /// and visible text. Re-scans periodically for the first few seconds to cover
165
168
  /// late-mounted shadow roots. Injected at document start in every frame.
166
169
  @objc public static func installBackButtonHider(on controller: WKUserContentController) {
170
+ guard shouldHideBackToInboxButton else { return }
171
+
167
172
  let script = WKUserScript(
168
173
  source: backButtonHiderJS,
169
174
  injectionTime: .atDocumentStart,
@@ -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.4.0",
3
+ "version": "0.4.1",
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",
package/src/index.ts CHANGED
@@ -7,6 +7,10 @@ export type SetIdentityParams = {
7
7
  email?: string | null;
8
8
  };
9
9
 
10
+ export type OpenChatOptions = {
11
+ hideBackToInboxButton?: boolean;
12
+ };
13
+
10
14
  function ensureNonEmpty(value: string, fieldName: string): void {
11
15
  if (!value || !value.trim()) {
12
16
  throw new Error(`\`${fieldName}\` must be a non-empty string.`);
@@ -18,9 +22,9 @@ const HubspotWrapper = {
18
22
  return NativeHubspotWrapper.initialize();
19
23
  },
20
24
 
21
- openChat(chatflow: string): Promise<void> {
25
+ openChat(chatflow: string, options: OpenChatOptions = {}): Promise<void> {
22
26
  ensureNonEmpty(chatflow, 'chatflow');
23
- return NativeHubspotWrapper.openChat(chatflow);
27
+ return NativeHubspotWrapper.openChat(chatflow, options.hideBackToInboxButton ?? true);
24
28
  },
25
29
 
26
30
  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>;
File without changes
@@ -1,2 +0,0 @@
1
- #Tue Apr 28 11:02:36 CEST 2026
2
- gradle.version=8.13
File without changes