@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.
- package/README.md +503 -0
- package/RNPylonChat.podspec +33 -0
- package/android/build.gradle +74 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/pylon/chatwidget/Pylon.kt +149 -0
- package/android/src/main/java/com/pylon/chatwidget/PylonChat.kt +715 -0
- package/android/src/main/java/com/pylon/chatwidget/PylonChatController.kt +63 -0
- package/android/src/main/java/com/pylon/chatwidget/PylonChatListener.kt +76 -0
- package/android/src/main/java/com/pylon/chatwidget/PylonChatView.kt +7 -0
- package/android/src/main/java/com/pylon/chatwidget/PylonConfig.kt +62 -0
- package/android/src/main/java/com/pylon/chatwidget/PylonDebugView.kt +76 -0
- package/android/src/main/java/com/pylon/chatwidget/PylonUser.kt +41 -0
- package/android/src/main/java/com/pylonchat/reactnative/RNPylonChatPackage.kt +17 -0
- package/android/src/main/java/com/pylonchat/reactnative/RNPylonChatView.kt +298 -0
- package/android/src/main/java/com/pylonchat/reactnative/RNPylonChatViewManager.kt +201 -0
- package/ios/PylonChat/PylonChat.swift +865 -0
- package/ios/RNPylonChatView.swift +332 -0
- package/ios/RNPylonChatViewManager.m +55 -0
- package/ios/RNPylonChatViewManager.swift +23 -0
- package/lib/PylonChatView.d.ts +27 -0
- package/lib/PylonChatView.js +78 -0
- package/lib/PylonChatWidget.android.d.ts +19 -0
- package/lib/PylonChatWidget.android.js +144 -0
- package/lib/PylonChatWidget.ios.d.ts +14 -0
- package/lib/PylonChatWidget.ios.js +79 -0
- package/lib/PylonModule.d.ts +32 -0
- package/lib/PylonModule.js +44 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +15 -0
- package/lib/types.d.ts +34 -0
- package/lib/types.js +2 -0
- package/package.json +39 -0
- package/src/PylonChatView.tsx +170 -0
- package/src/PylonChatWidget.android.tsx +165 -0
- package/src/PylonChatWidget.d.ts +15 -0
- package/src/PylonChatWidget.ios.tsx +79 -0
- package/src/PylonModule.ts +52 -0
- package/src/index.ts +15 -0
- 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";
|