@rematter/pylon-react-native 0.1.4

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.
Files changed (40) hide show
  1. package/README.md +503 -0
  2. package/RNPylonChat.podspec +33 -0
  3. package/android/build.gradle +74 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/main/AndroidManifest.xml +4 -0
  6. package/android/src/main/java/com/pylon/chatwidget/Pylon.kt +149 -0
  7. package/android/src/main/java/com/pylon/chatwidget/PylonChat.kt +715 -0
  8. package/android/src/main/java/com/pylon/chatwidget/PylonChatController.kt +63 -0
  9. package/android/src/main/java/com/pylon/chatwidget/PylonChatListener.kt +76 -0
  10. package/android/src/main/java/com/pylon/chatwidget/PylonChatView.kt +7 -0
  11. package/android/src/main/java/com/pylon/chatwidget/PylonConfig.kt +62 -0
  12. package/android/src/main/java/com/pylon/chatwidget/PylonDebugView.kt +76 -0
  13. package/android/src/main/java/com/pylon/chatwidget/PylonUser.kt +41 -0
  14. package/android/src/main/java/com/pylonchat/reactnative/RNPylonChatPackage.kt +17 -0
  15. package/android/src/main/java/com/pylonchat/reactnative/RNPylonChatView.kt +298 -0
  16. package/android/src/main/java/com/pylonchat/reactnative/RNPylonChatViewManager.kt +201 -0
  17. package/ios/PylonChat/PylonChat.swift +865 -0
  18. package/ios/RNPylonChatView.swift +332 -0
  19. package/ios/RNPylonChatViewManager.m +55 -0
  20. package/ios/RNPylonChatViewManager.swift +23 -0
  21. package/lib/PylonChatView.d.ts +27 -0
  22. package/lib/PylonChatView.js +78 -0
  23. package/lib/PylonChatWidget.android.d.ts +19 -0
  24. package/lib/PylonChatWidget.android.js +144 -0
  25. package/lib/PylonChatWidget.ios.d.ts +14 -0
  26. package/lib/PylonChatWidget.ios.js +79 -0
  27. package/lib/PylonModule.d.ts +32 -0
  28. package/lib/PylonModule.js +44 -0
  29. package/lib/index.d.ts +5 -0
  30. package/lib/index.js +15 -0
  31. package/lib/types.d.ts +34 -0
  32. package/lib/types.js +2 -0
  33. package/package.json +39 -0
  34. package/src/PylonChatView.tsx +170 -0
  35. package/src/PylonChatWidget.android.tsx +165 -0
  36. package/src/PylonChatWidget.d.ts +15 -0
  37. package/src/PylonChatWidget.ios.tsx +79 -0
  38. package/src/PylonModule.ts +52 -0
  39. package/src/index.ts +15 -0
  40. package/src/types.ts +37 -0
@@ -0,0 +1,63 @@
1
+ package com.pylon.chatwidget
2
+
3
+ /**
4
+ * Lightweight handle returned by [Pylon.createChat] so developers can interact
5
+ * with the underlying [PylonChat] view without keeping a direct reference to
6
+ * the view class (useful when wiring things in Jetpack Compose or XML layouts).
7
+ */
8
+ class PylonChatController internal constructor(
9
+ val view: PylonChat
10
+ ) {
11
+
12
+ fun setListener(listener: PylonChatListener?) {
13
+ view.setListener(listener)
14
+ }
15
+
16
+ fun openChat() {
17
+ view.openChat()
18
+ }
19
+
20
+ fun closeChat() {
21
+ view.closeChat()
22
+ }
23
+
24
+ fun showChatBubble() {
25
+ view.showChatBubble()
26
+ }
27
+
28
+ fun hideChatBubble() {
29
+ view.hideChatBubble()
30
+ }
31
+
32
+ fun updateUser(user: PylonUser) {
33
+ view.updateUser(user)
34
+ }
35
+
36
+ fun setNewIssueCustomFields(fields: Map<String, Any?>) {
37
+ view.setNewIssueCustomFields(fields)
38
+ }
39
+
40
+ fun setTicketFormFields(fields: Map<String, Any?>) {
41
+ view.setTicketFormFields(fields)
42
+ }
43
+
44
+ fun showNewMessage(message: String, isHtml: Boolean = false) {
45
+ view.showNewMessage(message, isHtml)
46
+ }
47
+
48
+ fun showTicketForm(ticketFormSlug: String) {
49
+ view.showTicketForm(ticketFormSlug)
50
+ }
51
+
52
+ fun showKnowledgeBaseArticle(articleId: String) {
53
+ view.showKnowledgeBaseArticle(articleId)
54
+ }
55
+
56
+ fun setEmailHash(emailHash: String?) {
57
+ view.updateEmailHash(emailHash)
58
+ }
59
+
60
+ fun destroy() {
61
+ view.destroy()
62
+ }
63
+ }
@@ -0,0 +1,76 @@
1
+ package com.pylon.chatwidget
2
+
3
+ /**
4
+ * Listener interface for Pylon widget events
5
+ */
6
+ interface PylonChatListener {
7
+ /**
8
+ * Called when the Pylon widget has finished loading
9
+ */
10
+ fun onPylonLoaded()
11
+
12
+ /**
13
+ * Called when Pylon is initialized with user data
14
+ */
15
+ fun onPylonInitialized()
16
+
17
+ /**
18
+ * Called when Pylon JavaScript is ready
19
+ */
20
+ fun onPylonReady()
21
+
22
+ /**
23
+ * Called when a message is received
24
+ */
25
+ fun onMessageReceived(message: String)
26
+
27
+ /**
28
+ * Called when the chat is opened
29
+ */
30
+ fun onChatOpened()
31
+
32
+ /**
33
+ * Called when the chat is closed
34
+ */
35
+ fun onChatClosed()
36
+
37
+ /**
38
+ * Called when there's an error loading Pylon
39
+ */
40
+ fun onPylonError(error: String)
41
+
42
+ /**
43
+ * Called when a file chooser is launched.
44
+ * This is only called when PylonChat is used from a non-Activity context.
45
+ *
46
+ * Developers can simply forward activity results to [Pylon.handleActivityResult]
47
+ * and ignore request codes altogether. The provided request code is only kept
48
+ * for backwards compatibility.
49
+ *
50
+ * @param requestCode The legacy request code identifier.
51
+ */
52
+ fun onFileChooserLaunched(requestCode: Int) {
53
+ // Default implementation - can be overridden if needed
54
+ }
55
+
56
+ /**
57
+ * Called whenever the unread message count changes.
58
+ */
59
+ fun onUnreadCountChanged(unreadCount: Int) {
60
+ // Default implementation - can be overridden if needed
61
+ }
62
+
63
+ /**
64
+ * Called whenever interactive element bounds change.
65
+ * Used by React Native to position proxy touch targets.
66
+ *
67
+ * @param selector The CSS selector of the interactive element
68
+ * @param left The left coordinate in pixels
69
+ * @param top The top coordinate in pixels
70
+ * @param right The right coordinate in pixels
71
+ * @param bottom The bottom coordinate in pixels
72
+ */
73
+ fun onInteractiveBoundsChanged(selector: String, left: Float, top: Float, right: Float, bottom: Float) {
74
+ // Default implementation - can be overridden if needed
75
+ }
76
+ }
@@ -0,0 +1,7 @@
1
+ package com.pylon.chatwidget
2
+
3
+ // Typealias for compatibility with React Native bridge.
4
+ // The Android SDK uses PylonChat, while iOS uses PylonChatView.
5
+ // This allows the RN bridge to use a consistent name across platforms.
6
+ typealias PylonChatView = PylonChat
7
+
@@ -0,0 +1,62 @@
1
+ package com.pylon.chatwidget
2
+
3
+ import java.net.URLEncoder
4
+
5
+ /**
6
+ * Immutable configuration for the Pylon SDK. Use the [Builder] to customise
7
+ * the settings and pass the result to [Pylon.initialize].
8
+ */
9
+ data class PylonConfig internal constructor(
10
+ val appId: String,
11
+ val enableLogging: Boolean,
12
+ val primaryColor: String?,
13
+ val debugMode: Boolean,
14
+ val widgetBaseUrl: String,
15
+ val widgetScriptUrl: String
16
+ ) {
17
+
18
+ class Builder internal constructor(private val appId: String) {
19
+ var enableLogging: Boolean = true
20
+ var primaryColor: String? = null
21
+ var debugMode: Boolean = false
22
+ var widgetBaseUrl: String = DEFAULT_WIDGET_BASE_URL
23
+ var widgetScriptUrl: String = defaultScriptUrl(appId)
24
+
25
+ internal fun build(): PylonConfig {
26
+ val scriptUrl = widgetScriptUrl.ifBlank { defaultScriptUrl(appId) }
27
+ return PylonConfig(
28
+ appId = appId,
29
+ enableLogging = enableLogging,
30
+ primaryColor = primaryColor,
31
+ debugMode = debugMode,
32
+ widgetBaseUrl = widgetBaseUrl.ifBlank { DEFAULT_WIDGET_BASE_URL },
33
+ widgetScriptUrl = scriptUrl
34
+ )
35
+ }
36
+ }
37
+
38
+ companion object {
39
+ private const val DEFAULT_WIDGET_BASE_URL = "https://widget.usepylon.com"
40
+
41
+ private fun defaultScriptUrl(appId: String): String {
42
+ val encodedAppId = URLEncoder.encode(appId, "UTF-8")
43
+ return "$DEFAULT_WIDGET_BASE_URL/widget/$encodedAppId"
44
+ }
45
+
46
+ fun build(appId: String, block: Builder.() -> Unit = {}): PylonConfig {
47
+ return Builder(appId).apply(block).build()
48
+ }
49
+
50
+ fun from(existing: PylonConfig, block: Builder.() -> Unit): PylonConfig {
51
+ val builder = Builder(existing.appId).apply {
52
+ enableLogging = existing.enableLogging
53
+ primaryColor = existing.primaryColor
54
+ debugMode = existing.debugMode
55
+ widgetBaseUrl = existing.widgetBaseUrl
56
+ widgetScriptUrl = existing.widgetScriptUrl
57
+ }
58
+ builder.block()
59
+ return builder.build()
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,76 @@
1
+ package com.pylon.chatwidget
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.Paint
7
+ import android.graphics.Rect
8
+ import android.view.View
9
+
10
+ class DebugOverlayView(context: Context) : View(context) {
11
+ private val paint = Paint().apply {
12
+ style = Paint.Style.STROKE
13
+ strokeWidth = 4f
14
+ }
15
+
16
+ private val fillPaint = Paint().apply {
17
+ style = Paint.Style.FILL
18
+ }
19
+
20
+ private val textPaint = Paint().apply {
21
+ textSize = 24f
22
+ isAntiAlias = true
23
+ setShadowLayer(2f, 0f, 0f, Color.BLACK) // Add shadow for readability
24
+ }
25
+
26
+ var bounds: Map<String, Rect> = emptyMap()
27
+ set(value) {
28
+ field = value
29
+ invalidate() // Trigger redraw
30
+ }
31
+
32
+ override fun onDraw(canvas: Canvas) {
33
+ super.onDraw(canvas)
34
+
35
+ bounds.forEach { (selector, rect) ->
36
+ if (!rect.isEmpty) {
37
+ val color = getColorForSelector(selector)
38
+
39
+ // Update paint colors
40
+ fillPaint.color = Color.argb(30, Color.red(color), Color.green(color), Color.blue(color))
41
+ paint.color = color
42
+ textPaint.color = color
43
+
44
+ // Draw filled rectangle
45
+ canvas.drawRect(rect, fillPaint)
46
+ // Draw border
47
+ canvas.drawRect(rect, paint)
48
+ // Draw label
49
+ canvas.drawText(
50
+ selector,
51
+ rect.left.toFloat(),
52
+ rect.top.toFloat() - 10f,
53
+ textPaint
54
+ )
55
+ }
56
+ }
57
+ }
58
+
59
+ private fun getColorForSelector(selector: String): Int {
60
+ // Generate consistent color from string hash
61
+ val hash = selector.hashCode()
62
+
63
+ // Use HSV to ensure colors are vibrant and distinct
64
+ val hue = (hash and 0xFFFF) % 360f // 0-359 degrees
65
+ val saturation = 0.7f + ((hash shr 16) and 0xFF) / 255f * 0.3f // 0.7-1.0
66
+ val value = 0.8f + ((hash shr 24) and 0xFF) / 255f * 0.2f // 0.8-1.0
67
+
68
+ return Color.HSVToColor(floatArrayOf(hue, saturation, value))
69
+ }
70
+
71
+ init {
72
+ setWillNotDraw(false) // Enable drawing
73
+ isClickable = false
74
+ isFocusable = false
75
+ }
76
+ }
@@ -0,0 +1,41 @@
1
+ package com.pylon.chatwidget
2
+
3
+ /**
4
+ * User information for Pylon Chat
5
+ */
6
+ data class PylonUser(
7
+ val email: String,
8
+ val name: String,
9
+ val avatarUrl: String? = null,
10
+ val emailHash: String? = null,
11
+ val accountId: String? = null,
12
+ val accountExternalId: String? = null
13
+ ) {
14
+
15
+ class Builder internal constructor(
16
+ private val email: String,
17
+ private val name: String
18
+ ) {
19
+ var avatarUrl: String? = null
20
+ var emailHash: String? = null
21
+ var accountId: String? = null
22
+ var accountExternalId: String? = null
23
+
24
+ fun build(): PylonUser = PylonUser(
25
+ email = email,
26
+ name = name,
27
+ avatarUrl = avatarUrl,
28
+ emailHash = emailHash,
29
+ accountId = accountId,
30
+ accountExternalId = accountExternalId
31
+ )
32
+ }
33
+
34
+ companion object {
35
+ @JvmStatic
36
+ @JvmOverloads
37
+ fun build(email: String, name: String, block: Builder.() -> Unit = {}): PylonUser {
38
+ return Builder(email, name).apply(block).build()
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,17 @@
1
+ package com.pylonchat.reactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class RNPylonChatPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return emptyList()
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return listOf(RNPylonChatViewManager())
15
+ }
16
+ }
17
+
@@ -0,0 +1,298 @@
1
+ package com.pylonchat.reactnative
2
+
3
+ import android.content.Context
4
+ import android.view.MotionEvent
5
+ import android.view.ViewGroup
6
+ import android.widget.FrameLayout
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.ReactContext
9
+ import com.facebook.react.bridge.WritableMap
10
+ import com.facebook.react.uimanager.events.RCTEventEmitter
11
+ import com.pylon.chatwidget.PylonChatListener
12
+ import com.pylon.chatwidget.PylonChatView
13
+ import com.pylon.chatwidget.PylonConfig
14
+ import com.pylon.chatwidget.PylonUser
15
+
16
+ /**
17
+ * React Native wrapper for PylonChatView.
18
+ * This is kept as minimal as possible to avoid interfering with touch pass-through.
19
+ */
20
+ class RNPylonChatView(context: Context) : FrameLayout(context) {
21
+
22
+ private var pylonChatView: PylonChatView? = null
23
+ private var config: PylonConfig? = null
24
+ private var user: PylonUser? = null
25
+
26
+ // Config properties
27
+ var appId: String? = null
28
+ set(value) {
29
+ field = value
30
+ updateConfig()
31
+ }
32
+
33
+ var widgetBaseUrl: String? = null
34
+ set(value) {
35
+ field = value
36
+ updateConfig()
37
+ }
38
+
39
+ var widgetScriptUrl: String? = null
40
+ set(value) {
41
+ field = value
42
+ updateConfig()
43
+ }
44
+
45
+ var enableLogging: Boolean = true
46
+ set(value) {
47
+ field = value
48
+ updateConfig()
49
+ }
50
+
51
+ var debugMode: Boolean = false
52
+ set(value) {
53
+ field = value
54
+ updateConfig()
55
+ }
56
+
57
+ var primaryColor: String? = null
58
+ set(value) {
59
+ field = value
60
+ updateConfig()
61
+ }
62
+
63
+ // User properties
64
+ var userEmail: String? = null
65
+ set(value) {
66
+ field = value
67
+ updateUser()
68
+ }
69
+
70
+ var userName: String? = null
71
+ set(value) {
72
+ field = value
73
+ updateUser()
74
+ }
75
+
76
+ var userAvatarUrl: String? = null
77
+ set(value) {
78
+ field = value
79
+ updateUser()
80
+ }
81
+
82
+ var userEmailHash: String? = null
83
+ set(value) {
84
+ field = value
85
+ updateUser()
86
+ }
87
+
88
+ var userAccountId: String? = null
89
+ set(value) {
90
+ field = value
91
+ updateUser()
92
+ }
93
+
94
+ var userAccountExternalId: String? = null
95
+ set(value) {
96
+ field = value
97
+ updateUser()
98
+ }
99
+
100
+ init {
101
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
102
+ }
103
+
104
+ // Track pointer events setting
105
+ private var pointerEventsMode = "auto"
106
+
107
+ fun setPointerEventsMode(mode: String) {
108
+ pointerEventsMode = mode
109
+ }
110
+
111
+ /**
112
+ * This is the CRITICAL method for pointerEvents.
113
+ * By returning false here when pointerEvents="none", we tell React Native's
114
+ * touch system to pass the touch to views BEHIND this one, not just children.
115
+ */
116
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
117
+ when (pointerEventsMode) {
118
+ "none" -> {
119
+ // Don't handle ANY touches - pass through to views BEHIND this one
120
+ return false
121
+ }
122
+ "box-none" -> {
123
+ // This view doesn't handle touches, but children can
124
+ // Try children first, if they don't handle it, pass through
125
+ val handled = super.dispatchTouchEvent(ev)
126
+ return handled // If children didn't handle it, return false passes through
127
+ }
128
+ "box-only" -> {
129
+ // Only this view handles touches, not children
130
+ // Don't dispatch to children
131
+ return onTouchEvent(ev)
132
+ }
133
+ else -> {
134
+ // "auto" - normal behavior
135
+ return super.dispatchTouchEvent(ev)
136
+ }
137
+ }
138
+ }
139
+
140
+ private fun updateConfig() {
141
+ val id = appId ?: return
142
+
143
+ config = PylonConfig.build(id) {
144
+ this.enableLogging = this@RNPylonChatView.enableLogging
145
+ this.primaryColor = this@RNPylonChatView.primaryColor
146
+ this.debugMode = this@RNPylonChatView.debugMode
147
+ this@RNPylonChatView.widgetBaseUrl?.let { this.widgetBaseUrl = it }
148
+ this@RNPylonChatView.widgetScriptUrl?.let { this.widgetScriptUrl = it }
149
+ }
150
+
151
+ recreatePylonView()
152
+ }
153
+
154
+ private fun updateUser() {
155
+ val email = userEmail ?: return
156
+ val name = userName ?: return
157
+
158
+ user = PylonUser(
159
+ email = email,
160
+ name = name,
161
+ avatarUrl = userAvatarUrl,
162
+ emailHash = userEmailHash,
163
+ accountId = userAccountId,
164
+ accountExternalId = userAccountExternalId
165
+ )
166
+
167
+ recreatePylonView()
168
+ }
169
+
170
+ private fun recreatePylonView() {
171
+ val cfg = config ?: return
172
+ val usr = user ?: return
173
+
174
+ // Remove old view
175
+ pylonChatView?.let { removeView(it) }
176
+
177
+ // Create new PylonChatView
178
+ val newView = PylonChatView(context, cfg, usr)
179
+ newView.setListener(object : PylonChatListener {
180
+ override fun onPylonLoaded() {
181
+ sendEvent("onPylonLoaded", Arguments.createMap())
182
+ }
183
+
184
+ override fun onPylonInitialized() {
185
+ sendEvent("onPylonInitialized", Arguments.createMap())
186
+ }
187
+
188
+ override fun onPylonReady() {
189
+ sendEvent("onPylonReady", Arguments.createMap())
190
+ }
191
+
192
+ override fun onMessageReceived(message: String) {
193
+ val params = Arguments.createMap()
194
+ params.putString("message", message)
195
+ sendEvent("onMessageReceived", params)
196
+ }
197
+
198
+ override fun onChatOpened() {
199
+ sendEvent("onChatOpened", Arguments.createMap())
200
+ }
201
+
202
+ override fun onChatClosed() {
203
+ val params = Arguments.createMap()
204
+ params.putBoolean("wasOpen", true)
205
+ sendEvent("onChatClosed", params)
206
+ }
207
+
208
+ override fun onInteractiveBoundsChanged(selector: String, left: Float, top: Float, right: Float, bottom: Float) {
209
+ // Convert pixels to density-independent pixels (dp) for React Native.
210
+ val density = resources.displayMetrics.density
211
+ val params = Arguments.createMap()
212
+ params.putString("selector", selector)
213
+ params.putDouble("left", (left / density).toDouble())
214
+ params.putDouble("top", (top / density).toDouble())
215
+ params.putDouble("right", (right / density).toDouble())
216
+ params.putDouble("bottom", (bottom / density).toDouble())
217
+ sendEvent("onInteractiveBoundsChanged", params)
218
+ }
219
+
220
+ override fun onPylonError(error: String) {
221
+ val params = Arguments.createMap()
222
+ params.putString("error", error)
223
+ sendEvent("onPylonError", params)
224
+ }
225
+
226
+ override fun onUnreadCountChanged(count: Int) {
227
+ val params = Arguments.createMap()
228
+ params.putInt("count", count)
229
+ sendEvent("onUnreadCountChanged", params)
230
+ }
231
+
232
+ override fun onFileChooserLaunched(requestCode: Int) {
233
+ // Handle file chooser if needed
234
+ }
235
+ })
236
+
237
+ newView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
238
+ addView(newView)
239
+ pylonChatView = newView
240
+ }
241
+
242
+ private fun sendEvent(eventName: String, params: WritableMap) {
243
+ val reactContext = context as ReactContext
244
+ reactContext
245
+ .getJSModule(RCTEventEmitter::class.java)
246
+ .receiveEvent(id, eventName, params)
247
+ }
248
+
249
+ // Imperative methods
250
+ fun openChat() {
251
+ pylonChatView?.openChat()
252
+ }
253
+
254
+ fun closeChat() {
255
+ pylonChatView?.closeChat()
256
+ }
257
+
258
+ fun showChatBubble() {
259
+ pylonChatView?.showChatBubble()
260
+ }
261
+
262
+ fun hideChatBubble() {
263
+ pylonChatView?.hideChatBubble()
264
+ }
265
+
266
+ fun showNewMessage(message: String, isHtml: Boolean) {
267
+ pylonChatView?.showNewMessage(message, isHtml)
268
+ }
269
+
270
+ fun setNewIssueCustomFields(fields: Map<String, Any?>) {
271
+ @Suppress("UNCHECKED_CAST")
272
+ pylonChatView?.setNewIssueCustomFields(fields as Map<String, Any>)
273
+ }
274
+
275
+ fun setTicketFormFields(fields: Map<String, Any?>) {
276
+ @Suppress("UNCHECKED_CAST")
277
+ pylonChatView?.setTicketFormFields(fields as Map<String, Any>)
278
+ }
279
+
280
+ fun updateEmailHash(emailHash: String?) {
281
+ pylonChatView?.setEmailHash(emailHash)
282
+ }
283
+
284
+ fun showTicketForm(slug: String) {
285
+ pylonChatView?.showTicketForm(slug)
286
+ }
287
+
288
+ fun showKnowledgeBaseArticle(articleId: String) {
289
+ pylonChatView?.showKnowledgeBaseArticle(articleId)
290
+ }
291
+
292
+ fun clickElementAtSelector(selector: String) {
293
+ // Trigger a click on the element with the given ID selector.
294
+ // This is used for Android's proxy-based touch pass-through system.
295
+ pylonChatView?.clickElementBySelector(selector)
296
+ }
297
+ }
298
+