@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,332 @@
1
+ //
2
+ // RNPylonChatView.swift
3
+ // RNPylonChat
4
+ //
5
+ // Wrapper around PylonChatView for React Native
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+ import React
11
+ import WebKit
12
+
13
+ // Import PylonChat from parent directory
14
+ // Note: PylonChat files will be added to Xcode project from ../../ios/PylonChat/
15
+
16
+ class RNPylonChatView: UIView {
17
+
18
+ private var pylonChatView: PylonChatView?
19
+ private var config: PylonConfig?
20
+ private var user: PylonUser?
21
+
22
+ // Config properties
23
+ @objc var appId: NSString = "" {
24
+ didSet { updateConfig() }
25
+ }
26
+
27
+ @objc var widgetBaseUrl: NSString? {
28
+ didSet { updateConfig() }
29
+ }
30
+
31
+ @objc var widgetScriptUrl: NSString? {
32
+ didSet { updateConfig() }
33
+ }
34
+
35
+ @objc var enableLogging: Bool = true {
36
+ didSet { updateConfig() }
37
+ }
38
+
39
+ @objc var debugMode: Bool = false {
40
+ didSet { updateConfig() }
41
+ }
42
+
43
+ @objc var primaryColor: NSString? {
44
+ didSet { updateConfig() }
45
+ }
46
+
47
+ // User properties
48
+ @objc var userEmail: NSString? {
49
+ didSet { updateUser() }
50
+ }
51
+
52
+ @objc var userName: NSString? {
53
+ didSet { updateUser() }
54
+ }
55
+
56
+ @objc var userAvatarUrl: NSString? {
57
+ didSet { updateUser() }
58
+ }
59
+
60
+ @objc var userEmailHash: NSString? {
61
+ didSet { updateUser() }
62
+ }
63
+
64
+ @objc var userAccountId: NSString? {
65
+ didSet { updateUser() }
66
+ }
67
+
68
+ @objc var userAccountExternalId: NSString? {
69
+ didSet { updateUser() }
70
+ }
71
+
72
+ // Safe area top inset for coordinate space adjustment
73
+ @objc var topInset: NSNumber = 0 {
74
+ didSet {
75
+ if let pylonView = pylonChatView {
76
+ pylonView.topInset = CGFloat(truncating: topInset)
77
+ }
78
+ }
79
+ }
80
+
81
+ // Event callbacks - renamed to avoid collision with PylonChatListener methods
82
+ @objc var rctOnPylonLoaded: RCTBubblingEventBlock?
83
+ @objc var rctOnPylonInitialized: RCTBubblingEventBlock?
84
+ @objc var rctOnPylonReady: RCTBubblingEventBlock?
85
+ @objc var rctOnChatOpened: RCTBubblingEventBlock?
86
+ @objc var rctOnChatClosed: RCTBubblingEventBlock?
87
+ @objc var rctOnUnreadCountChanged: RCTBubblingEventBlock?
88
+ @objc var rctOnMessageReceived: RCTBubblingEventBlock?
89
+ @objc var rctOnPylonError: RCTBubblingEventBlock?
90
+
91
+ override init(frame: CGRect) {
92
+ super.init(frame: frame)
93
+ setupView()
94
+ }
95
+
96
+ required init?(coder: NSCoder) {
97
+ super.init(coder: coder)
98
+ setupView()
99
+ }
100
+
101
+ private func setupView() {
102
+ backgroundColor = .clear
103
+ }
104
+
105
+ // Override pointInside to make React Native call hitTest
106
+ public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
107
+ // Always return true so React Native will call hitTest
108
+ // The actual hit detection happens in hitTest
109
+ return true
110
+ }
111
+
112
+ // Forward hit testing to the embedded PylonChatView
113
+ public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
114
+ // If we have a PylonChatView, let it handle hit testing
115
+ if let pylonView = pylonChatView {
116
+ // Convert point to pylonView's coordinate space
117
+ let convertedPoint = convert(point, to: pylonView)
118
+ return pylonView.hitTest(convertedPoint, with: event)
119
+ }
120
+
121
+ // If no PylonChatView yet, pass through (return nil)
122
+ return nil
123
+ }
124
+
125
+ private func updateConfig() {
126
+ guard (appId as String).isEmpty == false else { return }
127
+
128
+ config = PylonConfig(
129
+ appId: appId as String,
130
+ enableLogging: enableLogging,
131
+ primaryColor: primaryColor as String?,
132
+ debugMode: debugMode,
133
+ widgetBaseUrl: widgetBaseUrl as String?,
134
+ widgetScriptUrl: widgetScriptUrl as String?
135
+ )
136
+
137
+ recreatePylonView()
138
+ }
139
+
140
+ private func updateUser() {
141
+ guard let email = userEmail as String?,
142
+ let name = userName as String? else { return }
143
+
144
+ user = PylonUser(
145
+ email: email,
146
+ name: name,
147
+ avatarUrl: userAvatarUrl as String?,
148
+ emailHash: userEmailHash as String?,
149
+ accountId: userAccountId as String?,
150
+ accountExternalId: userAccountExternalId as String?
151
+ )
152
+
153
+ recreatePylonView()
154
+ }
155
+
156
+ private func recreatePylonView() {
157
+ guard let config = config, let user = user else { return }
158
+
159
+ // Remove old view
160
+ pylonChatView?.removeFromSuperview()
161
+
162
+ // Create new PylonChatView
163
+ let newView = PylonChatView(config: config, user: user)
164
+ newView.listener = self
165
+ newView.topInset = CGFloat(truncating: topInset)
166
+ newView.translatesAutoresizingMaskIntoConstraints = false
167
+
168
+ addSubview(newView)
169
+
170
+ NSLayoutConstraint.activate([
171
+ newView.topAnchor.constraint(equalTo: topAnchor),
172
+ newView.leadingAnchor.constraint(equalTo: leadingAnchor),
173
+ newView.trailingAnchor.constraint(equalTo: trailingAnchor),
174
+ newView.bottomAnchor.constraint(equalTo: bottomAnchor)
175
+ ])
176
+
177
+ pylonChatView = newView
178
+
179
+ // Force layout
180
+ setNeedsLayout()
181
+ layoutIfNeeded()
182
+ }
183
+
184
+ // Imperative methods (called from React Native)
185
+ func openChat() {
186
+ pylonChatView?.openChat()
187
+ }
188
+
189
+ func closeChat() {
190
+ pylonChatView?.closeChat()
191
+ }
192
+
193
+ func showChatBubble() {
194
+ pylonChatView?.showChatBubble()
195
+ }
196
+
197
+ func hideChatBubble() {
198
+ pylonChatView?.hideChatBubble()
199
+ }
200
+
201
+ func showNewMessage(_ message: String, isHtml: Bool) {
202
+ pylonChatView?.showNewMessage(message, isHtml: isHtml)
203
+ }
204
+
205
+ func setNewIssueCustomFields(_ fields: [String: Any]) {
206
+ pylonChatView?.setNewIssueCustomFields(fields)
207
+ }
208
+
209
+ func setTicketFormFields(_ fields: [String: Any]) {
210
+ pylonChatView?.setTicketFormFields(fields)
211
+ }
212
+
213
+ func updateEmailHash(_ emailHash: String?) {
214
+ pylonChatView?.updateEmailHash(emailHash)
215
+ }
216
+
217
+ func showTicketForm(_ slug: String) {
218
+ pylonChatView?.showTicketForm(slug)
219
+ }
220
+
221
+ func showKnowledgeBaseArticle(_ articleId: String) {
222
+ pylonChatView?.showKnowledgeBaseArticle(articleId)
223
+ }
224
+ }
225
+
226
+ // MARK: - PylonChatListener
227
+ extension RNPylonChatView: PylonChatListener {
228
+ func onPylonLoaded() {
229
+ rctOnPylonLoaded?([:])
230
+ }
231
+
232
+ func onPylonInitialized() {
233
+ rctOnPylonInitialized?([:])
234
+ }
235
+
236
+ func onPylonReady() {
237
+ rctOnPylonReady?([:])
238
+ }
239
+
240
+ func onMessageReceived(message: String) {
241
+ rctOnMessageReceived?(["message": message])
242
+ }
243
+
244
+ func onChatOpened() {
245
+ rctOnChatOpened?([:])
246
+ }
247
+
248
+ func onChatClosed(wasOpen: Bool) {
249
+ rctOnChatClosed?(["wasOpen": wasOpen])
250
+ }
251
+
252
+ func onPylonError(error: String) {
253
+ rctOnPylonError?(["error": error])
254
+ }
255
+
256
+ func onUnreadCountChanged(count: Int) {
257
+ rctOnUnreadCountChanged?(["count": count])
258
+ }
259
+ }
260
+
261
+ // MARK: - Imperative method helpers
262
+ extension RNPylonChatViewManager {
263
+ @objc func openChat(_ reactTag: NSNumber) {
264
+ bridge.uiManager.addUIBlock { _, viewRegistry in
265
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
266
+ view.openChat()
267
+ }
268
+ }
269
+
270
+ @objc func closeChat(_ reactTag: NSNumber) {
271
+ bridge.uiManager.addUIBlock { _, viewRegistry in
272
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
273
+ view.closeChat()
274
+ }
275
+ }
276
+
277
+ @objc func showChatBubble(_ reactTag: NSNumber) {
278
+ bridge.uiManager.addUIBlock { _, viewRegistry in
279
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
280
+ view.showChatBubble()
281
+ }
282
+ }
283
+
284
+ @objc func hideChatBubble(_ reactTag: NSNumber) {
285
+ bridge.uiManager.addUIBlock { _, viewRegistry in
286
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
287
+ view.hideChatBubble()
288
+ }
289
+ }
290
+
291
+ @objc func showNewMessage(_ reactTag: NSNumber, message: NSString, isHtml: Bool) {
292
+ bridge.uiManager.addUIBlock { _, viewRegistry in
293
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
294
+ view.showNewMessage(message as String, isHtml: isHtml)
295
+ }
296
+ }
297
+
298
+ @objc func setNewIssueCustomFields(_ reactTag: NSNumber, fields: NSDictionary) {
299
+ bridge.uiManager.addUIBlock { _, viewRegistry in
300
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
301
+ view.setNewIssueCustomFields(fields as! [String: Any])
302
+ }
303
+ }
304
+
305
+ @objc func setTicketFormFields(_ reactTag: NSNumber, fields: NSDictionary) {
306
+ bridge.uiManager.addUIBlock { _, viewRegistry in
307
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
308
+ view.setTicketFormFields(fields as! [String: Any])
309
+ }
310
+ }
311
+
312
+ @objc func updateEmailHash(_ reactTag: NSNumber, emailHash: NSString?) {
313
+ bridge.uiManager.addUIBlock { _, viewRegistry in
314
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
315
+ view.updateEmailHash(emailHash as String?)
316
+ }
317
+ }
318
+
319
+ @objc func showTicketForm(_ reactTag: NSNumber, slug: NSString) {
320
+ bridge.uiManager.addUIBlock { _, viewRegistry in
321
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
322
+ view.showTicketForm(slug as String)
323
+ }
324
+ }
325
+
326
+ @objc func showKnowledgeBaseArticle(_ reactTag: NSNumber, articleId: NSString) {
327
+ bridge.uiManager.addUIBlock { _, viewRegistry in
328
+ guard let view = viewRegistry?[reactTag] as? RNPylonChatView else { return }
329
+ view.showKnowledgeBaseArticle(articleId as String)
330
+ }
331
+ }
332
+ }
@@ -0,0 +1,55 @@
1
+ //
2
+ // RNPylonChatViewManager.m
3
+ // RNPylonChat
4
+ //
5
+ // React Native bridge to PylonChat iOS SDK
6
+ //
7
+
8
+ #import <React/RCTViewManager.h>
9
+ #import <React/RCTBridgeModule.h>
10
+
11
+ @interface RCT_EXTERN_MODULE(RNPylonChatViewManager, RCTViewManager)
12
+
13
+ // Config props
14
+ RCT_EXPORT_VIEW_PROPERTY(appId, NSString)
15
+ RCT_EXPORT_VIEW_PROPERTY(widgetBaseUrl, NSString)
16
+ RCT_EXPORT_VIEW_PROPERTY(widgetScriptUrl, NSString)
17
+ RCT_EXPORT_VIEW_PROPERTY(enableLogging, BOOL)
18
+ RCT_EXPORT_VIEW_PROPERTY(debugMode, BOOL)
19
+ RCT_EXPORT_VIEW_PROPERTY(primaryColor, NSString)
20
+
21
+ // User props
22
+ RCT_EXPORT_VIEW_PROPERTY(userEmail, NSString)
23
+ RCT_EXPORT_VIEW_PROPERTY(userName, NSString)
24
+ RCT_EXPORT_VIEW_PROPERTY(userAvatarUrl, NSString)
25
+ RCT_EXPORT_VIEW_PROPERTY(userEmailHash, NSString)
26
+ RCT_EXPORT_VIEW_PROPERTY(userAccountId, NSString)
27
+ RCT_EXPORT_VIEW_PROPERTY(userAccountExternalId, NSString)
28
+
29
+ // Coordinate space adjustment
30
+ RCT_EXPORT_VIEW_PROPERTY(topInset, NSNumber)
31
+
32
+ // Event callbacks - remap JS prop names (onX) to Swift property names (rctOnX) to avoid protocol collision
33
+ RCT_REMAP_VIEW_PROPERTY(onPylonLoaded, rctOnPylonLoaded, RCTBubblingEventBlock)
34
+ RCT_REMAP_VIEW_PROPERTY(onPylonInitialized, rctOnPylonInitialized, RCTBubblingEventBlock)
35
+ RCT_REMAP_VIEW_PROPERTY(onPylonReady, rctOnPylonReady, RCTBubblingEventBlock)
36
+ RCT_REMAP_VIEW_PROPERTY(onChatOpened, rctOnChatOpened, RCTBubblingEventBlock)
37
+ RCT_REMAP_VIEW_PROPERTY(onChatClosed, rctOnChatClosed, RCTBubblingEventBlock)
38
+ RCT_REMAP_VIEW_PROPERTY(onUnreadCountChanged, rctOnUnreadCountChanged, RCTBubblingEventBlock)
39
+ RCT_REMAP_VIEW_PROPERTY(onMessageReceived, rctOnMessageReceived, RCTBubblingEventBlock)
40
+ RCT_REMAP_VIEW_PROPERTY(onPylonError, rctOnPylonError, RCTBubblingEventBlock)
41
+
42
+ // Imperative methods
43
+ RCT_EXTERN_METHOD(openChat:(nonnull NSNumber *)reactTag)
44
+ RCT_EXTERN_METHOD(closeChat:(nonnull NSNumber *)reactTag)
45
+ RCT_EXTERN_METHOD(showChatBubble:(nonnull NSNumber *)reactTag)
46
+ RCT_EXTERN_METHOD(hideChatBubble:(nonnull NSNumber *)reactTag)
47
+ RCT_EXTERN_METHOD(showNewMessage:(nonnull NSNumber *)reactTag message:(NSString *)message isHtml:(BOOL)isHtml)
48
+ RCT_EXTERN_METHOD(setNewIssueCustomFields:(nonnull NSNumber *)reactTag fields:(NSDictionary *)fields)
49
+ RCT_EXTERN_METHOD(setTicketFormFields:(nonnull NSNumber *)reactTag fields:(NSDictionary *)fields)
50
+ RCT_EXTERN_METHOD(updateEmailHash:(nonnull NSNumber *)reactTag emailHash:(NSString *)emailHash)
51
+ RCT_EXTERN_METHOD(showTicketForm:(nonnull NSNumber *)reactTag slug:(NSString *)slug)
52
+ RCT_EXTERN_METHOD(showKnowledgeBaseArticle:(nonnull NSNumber *)reactTag articleId:(NSString *)articleId)
53
+
54
+ @end
55
+
@@ -0,0 +1,23 @@
1
+ //
2
+ // RNPylonChatViewManager.swift
3
+ // RNPylonChat
4
+ //
5
+ // React Native bridge to PylonChat iOS SDK
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+ import React
11
+
12
+ @objc(RNPylonChatViewManager)
13
+ class RNPylonChatViewManager: RCTViewManager {
14
+
15
+ override static func requiresMainQueueSetup() -> Bool {
16
+ return true
17
+ }
18
+
19
+ override func view() -> UIView! {
20
+ return RNPylonChatView()
21
+ }
22
+ }
23
+
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { ViewStyle } from "react-native";
3
+ import type { PylonChatListener, PylonConfig, PylonUser } from "./types";
4
+ export interface PylonChatViewRef {
5
+ openChat: () => void;
6
+ closeChat: () => void;
7
+ showChatBubble: () => void;
8
+ hideChatBubble: () => void;
9
+ showNewMessage: (message: string, isHtml?: boolean) => void;
10
+ setNewIssueCustomFields: (fields: Record<string, any>) => void;
11
+ setTicketFormFields: (fields: Record<string, any>) => void;
12
+ updateEmailHash: (emailHash: string | null) => void;
13
+ showTicketForm: (slug: string) => void;
14
+ showKnowledgeBaseArticle: (articleId: string) => void;
15
+ }
16
+ export interface PylonChatViewInternalRef extends PylonChatViewRef {
17
+ clickElementAtSelector: (selector: string) => void;
18
+ }
19
+ interface PylonChatViewProps {
20
+ config: PylonConfig;
21
+ user?: PylonUser;
22
+ style?: ViewStyle;
23
+ listener?: PylonChatListener;
24
+ topInset?: number;
25
+ }
26
+ export declare const PylonChatView: React.ForwardRefExoticComponent<PylonChatViewProps & React.RefAttributes<PylonChatViewInternalRef>>;
27
+ export default PylonChatView;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PylonChatView = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const NativePylonChatView = (0, react_native_1.requireNativeComponent)("RNPylonChatView");
40
+ const COMMANDS = {
41
+ openChat: "openChat",
42
+ closeChat: "closeChat",
43
+ showChatBubble: "showChatBubble",
44
+ hideChatBubble: "hideChatBubble",
45
+ showNewMessage: "showNewMessage",
46
+ setNewIssueCustomFields: "setNewIssueCustomFields",
47
+ setTicketFormFields: "setTicketFormFields",
48
+ updateEmailHash: "updateEmailHash",
49
+ showTicketForm: "showTicketForm",
50
+ showKnowledgeBaseArticle: "showKnowledgeBaseArticle",
51
+ clickElementAtSelector: "clickElementAtSelector",
52
+ };
53
+ exports.PylonChatView = react_1.default.forwardRef(({ config, user, style, listener, topInset = 0 }, ref) => {
54
+ const nativeRef = (0, react_1.useRef)(null);
55
+ const dispatchCommand = (commandName, args = []) => {
56
+ const handle = (0, react_native_1.findNodeHandle)(nativeRef.current);
57
+ if (handle) {
58
+ react_native_1.UIManager.dispatchViewManagerCommand(handle, commandName, args);
59
+ }
60
+ };
61
+ // Expose imperative methods via ref
62
+ (0, react_1.useImperativeHandle)(ref, () => ({
63
+ openChat: () => dispatchCommand(COMMANDS.openChat),
64
+ closeChat: () => dispatchCommand(COMMANDS.closeChat),
65
+ showChatBubble: () => dispatchCommand(COMMANDS.showChatBubble),
66
+ hideChatBubble: () => dispatchCommand(COMMANDS.hideChatBubble),
67
+ showNewMessage: (message, isHtml = false) => dispatchCommand(COMMANDS.showNewMessage, [message, isHtml]),
68
+ setNewIssueCustomFields: (fields) => dispatchCommand(COMMANDS.setNewIssueCustomFields, [fields]),
69
+ setTicketFormFields: (fields) => dispatchCommand(COMMANDS.setTicketFormFields, [fields]),
70
+ updateEmailHash: (emailHash) => dispatchCommand(COMMANDS.updateEmailHash, [emailHash]),
71
+ showTicketForm: (slug) => dispatchCommand(COMMANDS.showTicketForm, [slug]),
72
+ showKnowledgeBaseArticle: (articleId) => dispatchCommand(COMMANDS.showKnowledgeBaseArticle, [articleId]),
73
+ clickElementAtSelector: (selector) => dispatchCommand(COMMANDS.clickElementAtSelector, [selector]),
74
+ }), []);
75
+ return (<NativePylonChatView ref={nativeRef} style={style} appId={config.appId} widgetBaseUrl={config.widgetBaseUrl} widgetScriptUrl={config.widgetScriptUrl} enableLogging={config.enableLogging} debugMode={config.debugMode} primaryColor={config.primaryColor} userEmail={user?.email} userName={user?.name} userAvatarUrl={user?.avatarUrl} userEmailHash={user?.emailHash} userAccountId={user?.accountId} userAccountExternalId={user?.accountExternalId} topInset={topInset} onPylonLoaded={() => listener?.onPylonLoaded?.()} onPylonInitialized={() => listener?.onPylonInitialized?.()} onPylonReady={() => listener?.onPylonReady?.()} onChatOpened={() => listener?.onChatOpened?.()} onChatClosed={(event) => listener?.onChatClosed?.(event.nativeEvent.wasOpen)} onUnreadCountChanged={(event) => listener?.onUnreadCountChanged?.(event.nativeEvent.count)} onMessageReceived={(event) => listener?.onMessageReceived?.(event.nativeEvent.message)} onPylonError={(event) => listener?.onPylonError?.(event.nativeEvent.error)} onInteractiveBoundsChanged={(event) => listener?.onInteractiveBoundsChanged?.(event.nativeEvent)}/>);
76
+ });
77
+ exports.PylonChatView.displayName = "PylonChatView";
78
+ exports.default = exports.PylonChatView;
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import type { PylonChatViewRef } from "./PylonChatView";
3
+ import type { PylonChatWidgetProps } from "./PylonChatWidget";
4
+ /**
5
+ * Android implementation using proxy-based touch pass-through.
6
+ *
7
+ * State Management:
8
+ * - isChatOpen is ONLY set by native events (onChatOpened/onChatClosed)
9
+ * - Imperative methods (openChat/closeChat) call native, which then fires events
10
+ * - This ensures state is always synced with native layer
11
+ *
12
+ * Touch Pass-Through Strategy:
13
+ * 1. Native reports interactive element positions via onInteractiveBoundsChanged
14
+ * 2. React renders clickable Pressable views at those positions
15
+ * 3. When chat is closed: WebView has pointerEvents="none" (passes touches through)
16
+ * 4. Touches hit background OR proxy
17
+ * 5. Proxy clicked → calls openChat() → fires onChatOpened → state updates → WebView enabled
18
+ */
19
+ export declare const PylonChatWidget: React.ForwardRefExoticComponent<PylonChatWidgetProps & React.RefAttributes<PylonChatViewRef>>;
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PylonChatWidget = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const PylonChatView_1 = require("./PylonChatView");
40
+ /**
41
+ * Android implementation using proxy-based touch pass-through.
42
+ *
43
+ * State Management:
44
+ * - isChatOpen is ONLY set by native events (onChatOpened/onChatClosed)
45
+ * - Imperative methods (openChat/closeChat) call native, which then fires events
46
+ * - This ensures state is always synced with native layer
47
+ *
48
+ * Touch Pass-Through Strategy:
49
+ * 1. Native reports interactive element positions via onInteractiveBoundsChanged
50
+ * 2. React renders clickable Pressable views at those positions
51
+ * 3. When chat is closed: WebView has pointerEvents="none" (passes touches through)
52
+ * 4. Touches hit background OR proxy
53
+ * 5. Proxy clicked → calls openChat() → fires onChatOpened → state updates → WebView enabled
54
+ */
55
+ exports.PylonChatWidget = (0, react_1.forwardRef)(({ config, user, listener, style, topInset }, ref) => {
56
+ // State synced ONLY via native events - single source of truth
57
+ // TODO: Consider useSyncExternalStore for a more resilient solution in the future.
58
+ const [isChatOpen, setIsChatOpen] = (0, react_1.useState)(false);
59
+ const [interactiveBounds, setInteractiveBounds] = (0, react_1.useState)([]);
60
+ // Internal ref has additional methods not exposed to SDK users.
61
+ const chatRef = (0, react_1.useRef)(null);
62
+ // Forward ref methods - these call native, which fires events that update state
63
+ // CRITICAL: DO NOT set state directly here - let events handle it
64
+ (0, react_1.useImperativeHandle)(ref, () => ({
65
+ openChat: () => {
66
+ chatRef.current?.openChat();
67
+ },
68
+ closeChat: () => {
69
+ chatRef.current?.closeChat();
70
+ },
71
+ showChatBubble: () => chatRef.current?.showChatBubble(),
72
+ hideChatBubble: () => chatRef.current?.hideChatBubble(),
73
+ showNewMessage: (message, isHtml) => chatRef.current?.showNewMessage(message, isHtml),
74
+ setNewIssueCustomFields: (fields) => chatRef.current?.setNewIssueCustomFields(fields),
75
+ setTicketFormFields: (fields) => chatRef.current?.setTicketFormFields(fields),
76
+ updateEmailHash: (emailHash) => chatRef.current?.updateEmailHash(emailHash),
77
+ showTicketForm: (slug) => chatRef.current?.showTicketForm(slug),
78
+ showKnowledgeBaseArticle: (articleId) => chatRef.current?.showKnowledgeBaseArticle(articleId),
79
+ }));
80
+ // CRITICAL: State is ONLY updated by these event handlers.
81
+ // The flow is: imperative method → native JS call → Pylon widget event → native callback → React event.
82
+ const handleChatOpened = (0, react_1.useCallback)(() => {
83
+ setIsChatOpen(true);
84
+ listener?.onChatOpened?.();
85
+ }, [listener]);
86
+ const handleChatClosed = (0, react_1.useCallback)((wasOpen) => {
87
+ setIsChatOpen(false);
88
+ listener?.onChatClosed?.(wasOpen);
89
+ }, [listener]);
90
+ const handleBoundsChanged = (0, react_1.useCallback)((bounds) => {
91
+ setInteractiveBounds((prev) => {
92
+ const existing = prev.findIndex((b) => b.selector === bounds.selector);
93
+ // If bounds are 0,0,0,0 it means element is hidden - remove it
94
+ const isHidden = bounds.left === 0 &&
95
+ bounds.top === 0 &&
96
+ bounds.right === 0 &&
97
+ bounds.bottom === 0;
98
+ if (existing >= 0) {
99
+ const updated = [...prev];
100
+ if (isHidden) {
101
+ updated.splice(existing, 1);
102
+ }
103
+ else {
104
+ updated[existing] = bounds;
105
+ }
106
+ return updated;
107
+ }
108
+ if (isHidden) {
109
+ return prev;
110
+ }
111
+ return [...prev, bounds];
112
+ });
113
+ }, []);
114
+ const handleProxyPress = (0, react_1.useCallback)((selector) => {
115
+ // Trigger a click on the WebView element by its ID selector.
116
+ // This kind of only works for areas with a single clickable element.
117
+ // Really what htis needs to become is pass a coordinate to the webview, we look up whatever thing is at that
118
+ // coordinate, and click it. Which is very sophisticated and hacky.
119
+ chatRef.current?.clickElementAtSelector(selector);
120
+ }, []);
121
+ return (<>
122
+ <react_native_1.View style={[react_native_1.StyleSheet.absoluteFill, style]} pointerEvents={isChatOpen ? "auto" : "none"}>
123
+ {/* The actual WebView - disabled when chat is closed */}
124
+ <PylonChatView_1.PylonChatView ref={chatRef} style={react_native_1.StyleSheet.absoluteFillObject} config={config} user={user} topInset={topInset} listener={{
125
+ ...listener,
126
+ onChatOpened: handleChatOpened,
127
+ onChatClosed: handleChatClosed,
128
+ onInteractiveBoundsChanged: handleBoundsChanged,
129
+ }}/>
130
+ </react_native_1.View>
131
+ {!isChatOpen &&
132
+ interactiveBounds.map((bounds, index) => (<react_native_1.Pressable key={`${bounds.selector}-${index}`} style={{
133
+ position: "absolute",
134
+ left: bounds.left,
135
+ top: bounds.top,
136
+ width: bounds.right - bounds.left,
137
+ height: bounds.bottom - bounds.top,
138
+ backgroundColor: __DEV__ && config.debugMode ? "rgba(0,255,0,0.2)" : undefined,
139
+ borderWidth: __DEV__ && config.debugMode ? 2 : 0,
140
+ borderColor: __DEV__ && config.debugMode ? "cyan" : undefined,
141
+ }} onPress={() => handleProxyPress(bounds.selector)}/>))}
142
+ </>);
143
+ });
144
+ exports.PylonChatWidget.displayName = "PylonChatWidget";