react-native-hubspot-wrapper 0.2.0 → 0.3.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.
File without changes
@@ -0,0 +1,2 @@
1
+ #Tue Apr 28 11:02:36 CEST 2026
2
+ gradle.version=8.13
File without changes
@@ -1,6 +1,7 @@
1
1
  package com.marcinolek.reactnativehubspotwrapper
2
2
 
3
3
  import android.content.Intent
4
+ import android.util.Log
4
5
  import android.webkit.CookieManager
5
6
  import com.facebook.react.bridge.Promise
6
7
  import com.facebook.react.bridge.ReactApplicationContext
@@ -72,16 +73,94 @@ class HubspotWrapperModule(reactContext: ReactApplicationContext) :
72
73
  override fun clearUserData(promise: Promise) {
73
74
  CoroutineScope(Dispatchers.Main).launch {
74
75
  try {
76
+ Log.i(TAG, "clearUserData: invoked")
75
77
  hubspotManager.logout()
76
- clearWebViewCookies()
78
+ clearHubspotSessionCookies()
79
+ Log.i(TAG, "clearUserData: done")
77
80
  promise.resolve(null)
78
81
  } catch (error: Exception) {
82
+ Log.e(TAG, "clearUserData: failed", error)
79
83
  promise.reject("CLEAR_USER_DATA_ERROR", "Failed to clear HubSpot user data", error)
80
84
  }
81
85
  }
82
86
  }
83
87
 
84
- private suspend fun clearWebViewCookies() {
88
+ /**
89
+ * Mirror the iOS SDK's `cookiesToDeleteWhenClearingData` behavior: only drop the cookies that
90
+ * tie the embedded chat WebView to a previous HubSpot visitor identity (`hubspotutk`,
91
+ * `messagesUtk`). Anything else - Cloudflare bot-management cookies, future preference cookies,
92
+ * etc. - is intentionally preserved so the next chat session starts with a fresh identity but
93
+ * doesn't pay any unrelated cold-start tax.
94
+ *
95
+ * Implementation notes for Android:
96
+ *
97
+ * - `CookieManager.getCookie(url)` only returns `name=value` pairs - no `Domain`/`Path`
98
+ * metadata - so to delete a cookie reliably we have to overwrite it with an expired date
99
+ * under every plausible scope (host-only, exact host `Domain`, parent `Domain`). Writes
100
+ * whose `Domain` doesn't match an existing cookie are silently rejected by the cookie store,
101
+ * so issuing all three is safe.
102
+ *
103
+ * - The chat URL's host is computed from `hubspot-info.json` using the same rule as the SDK's
104
+ * internal `Hublet`/`Environment` value classes (which are package-private):
105
+ *
106
+ * `https://app[-<hublet>].hubspot[qa].com/`
107
+ *
108
+ * For example: hublet `na2` + env `prod` -> `https://app-na2.hubspot.com/`.
109
+ * Hublet `na1` is special-cased to `app.hubspot.com`. Env `qa` adds the `qa` suffix
110
+ * to the apex (`hubspotqa.com`).
111
+ *
112
+ * - If `HubspotManager` hasn't been configured yet, we fall back to `removeAllCookies()` so
113
+ * we never leak chat identity even in misconfigured states.
114
+ *
115
+ * Caveat (matches iOS): clearing `messagesUtk` resets the visitor identity, which causes
116
+ * HubSpot's chat embed to re-show its cookie consent banner on the next session. This is by
117
+ * design on HubSpot's side - their chat treats every new visitor id as a new visitor that
118
+ * must consent. There is no way to keep both "fresh chat" and "no consent reprompt" without
119
+ * also leaving the previous conversation thread visible to the user.
120
+ */
121
+ private suspend fun clearHubspotSessionCookies() {
122
+ val hubletId = runCatching { hubspotManager.getHublet() }.getOrNull()
123
+ val environment = runCatching { hubspotManager.getEnvironment() }.getOrNull()
124
+
125
+ if (hubletId.isNullOrEmpty() || environment.isNullOrEmpty()) {
126
+ Log.w(TAG, "clearUserData: missing hublet/environment, falling back to removeAllCookies()")
127
+ clearAllWebViewCookies()
128
+ return
129
+ }
130
+
131
+ val cookieManager = CookieManager.getInstance()
132
+ val urlSuffix = if (environment.equals("qa", ignoreCase = true)) "qa" else ""
133
+ val rootDomain = "hubspot$urlSuffix.com"
134
+ val appsSubDomain = if (hubletId.equals("na1", ignoreCase = true)) "app" else "app-$hubletId"
135
+ val chatHost = "$appsSubDomain.$rootDomain"
136
+ val chatUrl = "https://$chatHost/"
137
+
138
+ val existingCookieNames = cookieManager.getCookie(chatUrl).orEmpty()
139
+ .split(";")
140
+ .mapNotNull { it.substringBefore("=").trim().takeIf(String::isNotEmpty) }
141
+ .toSet()
142
+
143
+ val toDelete = COOKIES_TO_CLEAR.filter { it in existingCookieNames }
144
+ if (toDelete.isEmpty()) {
145
+ Log.i(TAG, "clearUserData: no chat-identity cookies present at $chatHost, nothing to delete")
146
+ return
147
+ }
148
+
149
+ Log.i(TAG, "clearUserData: deleting cookies=$toDelete on $chatHost")
150
+ val expiry = "Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
151
+ toDelete.forEach { name ->
152
+ cookieManager.setCookie(chatUrl, "$name=; $expiry")
153
+ cookieManager.setCookie(chatUrl, "$name=; Domain=$chatHost; $expiry")
154
+ cookieManager.setCookie(chatUrl, "$name=; Domain=.$rootDomain; $expiry")
155
+ }
156
+
157
+ suspendCancellableCoroutine<Unit> { continuation ->
158
+ cookieManager.flush()
159
+ continuation.resume(Unit)
160
+ }
161
+ }
162
+
163
+ private suspend fun clearAllWebViewCookies() {
85
164
  suspendCancellableCoroutine<Unit> { continuation ->
86
165
  val cookieManager = CookieManager.getInstance()
87
166
  cookieManager.removeAllCookies {
@@ -93,5 +172,14 @@ class HubspotWrapperModule(reactContext: ReactApplicationContext) :
93
172
 
94
173
  companion object {
95
174
  const val NAME = "NativeHubspotWrapper"
175
+
176
+ private const val TAG = "HubspotWrapper"
177
+
178
+ /**
179
+ * Cookies that bind the embedded chat WebView to a specific HubSpot visitor identity.
180
+ * Matches the iOS SDK's `cookiesToDeleteWhenClearingData`.
181
+ * See https://knowledge.hubspot.com/privacy-and-consent/what-cookies-does-hubspot-set-in-a-visitor-s-browser
182
+ */
183
+ private val COOKIES_TO_CLEAR = listOf("hubspotutk", "messagesUtk")
96
184
  }
97
185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-hubspot-wrapper",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "TurboModule wrapper for HubSpot mobile chat SDK",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",