@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,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { PylonChatViewRef } from "./PylonChatView";
|
|
3
|
+
import type { PylonChatWidgetProps } from "./PylonChatWidget";
|
|
4
|
+
/**
|
|
5
|
+
* iOS implementation - simple passthrough.
|
|
6
|
+
*
|
|
7
|
+
* iOS uses native hitTest for touch pass-through, so no proxy logic needed.
|
|
8
|
+
*
|
|
9
|
+
* State Management:
|
|
10
|
+
* - State lives entirely in native layer (no React state needed for iOS)
|
|
11
|
+
* - Imperative methods call native, which handles everything
|
|
12
|
+
* - Events are simply forwarded to user's listener
|
|
13
|
+
*/
|
|
14
|
+
export declare const PylonChatWidget: React.ForwardRefExoticComponent<PylonChatWidgetProps & React.RefAttributes<PylonChatViewRef>>;
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
* iOS implementation - simple passthrough.
|
|
42
|
+
*
|
|
43
|
+
* iOS uses native hitTest for touch pass-through, so no proxy logic needed.
|
|
44
|
+
*
|
|
45
|
+
* State Management:
|
|
46
|
+
* - State lives entirely in native layer (no React state needed for iOS)
|
|
47
|
+
* - Imperative methods call native, which handles everything
|
|
48
|
+
* - Events are simply forwarded to user's listener
|
|
49
|
+
*/
|
|
50
|
+
exports.PylonChatWidget = (0, react_1.forwardRef)(({ config, user, listener, style, topInset }, ref) => {
|
|
51
|
+
// Internal ref - typed as any since iOS doesn't need clickElementAtSelector.
|
|
52
|
+
const chatRef = (0, react_1.useRef)(null);
|
|
53
|
+
// Forward ref methods - all state managed in native layer
|
|
54
|
+
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
55
|
+
openChat: () => chatRef.current?.openChat(),
|
|
56
|
+
closeChat: () => chatRef.current?.closeChat(),
|
|
57
|
+
showChatBubble: () => chatRef.current?.showChatBubble(),
|
|
58
|
+
hideChatBubble: () => chatRef.current?.hideChatBubble(),
|
|
59
|
+
showNewMessage: (message, isHtml) => chatRef.current?.showNewMessage(message, isHtml),
|
|
60
|
+
setNewIssueCustomFields: (fields) => chatRef.current?.setNewIssueCustomFields(fields),
|
|
61
|
+
setTicketFormFields: (fields) => chatRef.current?.setTicketFormFields(fields),
|
|
62
|
+
updateEmailHash: (emailHash) => chatRef.current?.updateEmailHash(emailHash),
|
|
63
|
+
showTicketForm: (slug) => chatRef.current?.showTicketForm(slug),
|
|
64
|
+
showKnowledgeBaseArticle: (articleId) => chatRef.current?.showKnowledgeBaseArticle(articleId),
|
|
65
|
+
}));
|
|
66
|
+
// Event handlers - forward to user's listener
|
|
67
|
+
const handleChatOpened = (0, react_1.useCallback)(() => {
|
|
68
|
+
listener?.onChatOpened?.();
|
|
69
|
+
}, [listener]);
|
|
70
|
+
const handleChatClosed = (0, react_1.useCallback)((wasOpen) => {
|
|
71
|
+
listener?.onChatClosed?.(wasOpen);
|
|
72
|
+
}, [listener]);
|
|
73
|
+
return (<PylonChatView_1.PylonChatView ref={chatRef} style={style || react_native_1.StyleSheet.absoluteFillObject} config={config} user={user} topInset={topInset} listener={{
|
|
74
|
+
...listener,
|
|
75
|
+
onChatOpened: handleChatOpened,
|
|
76
|
+
onChatClosed: handleChatClosed,
|
|
77
|
+
}}/>);
|
|
78
|
+
});
|
|
79
|
+
exports.PylonChatWidget.displayName = "PylonChatWidget";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PylonConfig, PylonUser } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Pylon SDK - Singleton for managing global configuration.
|
|
4
|
+
*
|
|
5
|
+
* Note: This is optional - you can also pass config directly to PylonChatView.
|
|
6
|
+
* This singleton is useful if you want to initialize once and reuse across multiple views.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Pylon {
|
|
9
|
+
private static instance;
|
|
10
|
+
private _config?;
|
|
11
|
+
private _user?;
|
|
12
|
+
private constructor();
|
|
13
|
+
static get shared(): Pylon;
|
|
14
|
+
/**
|
|
15
|
+
* Initialize the Pylon SDK with configuration.
|
|
16
|
+
*/
|
|
17
|
+
initialize(config: PylonConfig): void;
|
|
18
|
+
/**
|
|
19
|
+
* Set the current user for the chat.
|
|
20
|
+
*/
|
|
21
|
+
setUser(user: PylonUser): void;
|
|
22
|
+
/**
|
|
23
|
+
* Get the current configuration.
|
|
24
|
+
*/
|
|
25
|
+
get config(): PylonConfig | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Get the current user.
|
|
28
|
+
*/
|
|
29
|
+
get user(): PylonUser | undefined;
|
|
30
|
+
}
|
|
31
|
+
declare const _default: Pylon;
|
|
32
|
+
export default _default;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Pylon = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Pylon SDK - Singleton for managing global configuration.
|
|
6
|
+
*
|
|
7
|
+
* Note: This is optional - you can also pass config directly to PylonChatView.
|
|
8
|
+
* This singleton is useful if you want to initialize once and reuse across multiple views.
|
|
9
|
+
*/
|
|
10
|
+
class Pylon {
|
|
11
|
+
constructor() { }
|
|
12
|
+
static get shared() {
|
|
13
|
+
if (!Pylon.instance) {
|
|
14
|
+
Pylon.instance = new Pylon();
|
|
15
|
+
}
|
|
16
|
+
return Pylon.instance;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the Pylon SDK with configuration.
|
|
20
|
+
*/
|
|
21
|
+
initialize(config) {
|
|
22
|
+
this._config = config;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Set the current user for the chat.
|
|
26
|
+
*/
|
|
27
|
+
setUser(user) {
|
|
28
|
+
this._user = user;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get the current configuration.
|
|
32
|
+
*/
|
|
33
|
+
get config() {
|
|
34
|
+
return this._config;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the current user.
|
|
38
|
+
*/
|
|
39
|
+
get user() {
|
|
40
|
+
return this._user;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.Pylon = Pylon;
|
|
44
|
+
exports.default = Pylon.shared;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { PylonChatView } from "./PylonChatView";
|
|
2
|
+
export type { PylonChatViewRef, PylonChatViewInternalRef, } from "./PylonChatView";
|
|
3
|
+
export { PylonChatWidget } from "./PylonChatWidget";
|
|
4
|
+
export { Pylon, default as PylonSDK } from "./PylonModule";
|
|
5
|
+
export type { InteractiveBound, PylonChatListener, PylonConfig, PylonUser, } from "./types";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PylonSDK = exports.Pylon = exports.PylonChatWidget = exports.PylonChatView = void 0;
|
|
7
|
+
var PylonChatView_1 = require("./PylonChatView");
|
|
8
|
+
Object.defineProperty(exports, "PylonChatView", { enumerable: true, get: function () { return PylonChatView_1.PylonChatView; } });
|
|
9
|
+
// React Native will automatically resolve to .ios.tsx or .android.tsx at runtime
|
|
10
|
+
// TypeScript just needs to find the types from one of them
|
|
11
|
+
var PylonChatWidget_1 = require("./PylonChatWidget");
|
|
12
|
+
Object.defineProperty(exports, "PylonChatWidget", { enumerable: true, get: function () { return PylonChatWidget_1.PylonChatWidget; } });
|
|
13
|
+
var PylonModule_1 = require("./PylonModule");
|
|
14
|
+
Object.defineProperty(exports, "Pylon", { enumerable: true, get: function () { return PylonModule_1.Pylon; } });
|
|
15
|
+
Object.defineProperty(exports, "PylonSDK", { enumerable: true, get: function () { return __importDefault(PylonModule_1).default; } });
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface PylonConfig {
|
|
2
|
+
appId: string;
|
|
3
|
+
enableLogging?: boolean;
|
|
4
|
+
primaryColor?: string;
|
|
5
|
+
debugMode?: boolean;
|
|
6
|
+
widgetBaseUrl?: string;
|
|
7
|
+
widgetScriptUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PylonUser {
|
|
10
|
+
email: string;
|
|
11
|
+
name: string;
|
|
12
|
+
avatarUrl?: string;
|
|
13
|
+
emailHash?: string;
|
|
14
|
+
accountId?: string;
|
|
15
|
+
accountExternalId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface InteractiveBound {
|
|
18
|
+
selector: string;
|
|
19
|
+
left: number;
|
|
20
|
+
top: number;
|
|
21
|
+
right: number;
|
|
22
|
+
bottom: number;
|
|
23
|
+
}
|
|
24
|
+
export interface PylonChatListener {
|
|
25
|
+
onPylonLoaded?: () => void;
|
|
26
|
+
onPylonInitialized?: () => void;
|
|
27
|
+
onPylonReady?: () => void;
|
|
28
|
+
onMessageReceived?: (message: string) => void;
|
|
29
|
+
onChatOpened?: () => void;
|
|
30
|
+
onChatClosed?: (wasOpen: boolean) => void;
|
|
31
|
+
onPylonError?: (error: string) => void;
|
|
32
|
+
onUnreadCountChanged?: (count: number) => void;
|
|
33
|
+
onInteractiveBoundsChanged?: (bounds: InteractiveBound) => void;
|
|
34
|
+
}
|
package/lib/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rematter/pylon-react-native",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Pylon Chat SDK for React Native",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"homepage": "https://github.com/usepylon/pylon-chat-sdk",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"watch": "tsc --watch",
|
|
11
|
+
"copy-sdks": "bash scripts/copy-native-sdks.sh",
|
|
12
|
+
"prepare": "npm run copy-sdks && npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pylon",
|
|
16
|
+
"chat",
|
|
17
|
+
"react-native",
|
|
18
|
+
"customer-support",
|
|
19
|
+
"native-module"
|
|
20
|
+
],
|
|
21
|
+
"author": "Pylon",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": ">=18.0.0",
|
|
25
|
+
"react-native": ">=0.70.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": "^18.2.0",
|
|
29
|
+
"@types/react-native": "^0.72.0",
|
|
30
|
+
"typescript": "^5.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib",
|
|
34
|
+
"src",
|
|
35
|
+
"ios",
|
|
36
|
+
"android",
|
|
37
|
+
"RNPylonChat.podspec"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useImperativeHandle, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
findNodeHandle,
|
|
4
|
+
requireNativeComponent,
|
|
5
|
+
UIManager,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import type { PylonChatListener, PylonConfig, PylonUser } from "./types";
|
|
9
|
+
|
|
10
|
+
// Public API exposed to SDK users.
|
|
11
|
+
export interface PylonChatViewRef {
|
|
12
|
+
openChat: () => void;
|
|
13
|
+
closeChat: () => void;
|
|
14
|
+
showChatBubble: () => void;
|
|
15
|
+
hideChatBubble: () => void;
|
|
16
|
+
showNewMessage: (message: string, isHtml?: boolean) => void;
|
|
17
|
+
setNewIssueCustomFields: (fields: Record<string, any>) => void;
|
|
18
|
+
setTicketFormFields: (fields: Record<string, any>) => void;
|
|
19
|
+
updateEmailHash: (emailHash: string | null) => void;
|
|
20
|
+
showTicketForm: (slug: string) => void;
|
|
21
|
+
showKnowledgeBaseArticle: (articleId: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Internal interface with additional methods for platform-specific implementations.
|
|
25
|
+
export interface PylonChatViewInternalRef extends PylonChatViewRef {
|
|
26
|
+
clickElementAtSelector: (selector: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PylonChatViewProps {
|
|
30
|
+
config: PylonConfig;
|
|
31
|
+
user?: PylonUser;
|
|
32
|
+
style?: ViewStyle;
|
|
33
|
+
listener?: PylonChatListener;
|
|
34
|
+
topInset?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface NativePylonChatViewProps {
|
|
38
|
+
style?: ViewStyle;
|
|
39
|
+
pointerEvents?: "box-none" | "none" | "box-only" | "auto";
|
|
40
|
+
appId: string;
|
|
41
|
+
widgetBaseUrl?: string;
|
|
42
|
+
widgetScriptUrl?: string;
|
|
43
|
+
enableLogging?: boolean;
|
|
44
|
+
debugMode?: boolean;
|
|
45
|
+
primaryColor?: string;
|
|
46
|
+
userEmail?: string;
|
|
47
|
+
userName?: string;
|
|
48
|
+
userAvatarUrl?: string;
|
|
49
|
+
userEmailHash?: string;
|
|
50
|
+
userAccountId?: string;
|
|
51
|
+
userAccountExternalId?: string;
|
|
52
|
+
topInset?: number;
|
|
53
|
+
onPylonLoaded?: () => void;
|
|
54
|
+
onPylonInitialized?: () => void;
|
|
55
|
+
onPylonReady?: () => void;
|
|
56
|
+
onChatOpened?: () => void;
|
|
57
|
+
onChatClosed?: (event: { nativeEvent: { wasOpen: boolean } }) => void;
|
|
58
|
+
onUnreadCountChanged?: (event: { nativeEvent: { count: number } }) => void;
|
|
59
|
+
onMessageReceived?: (event: { nativeEvent: { message: string } }) => void;
|
|
60
|
+
onPylonError?: (event: { nativeEvent: { error: string } }) => void;
|
|
61
|
+
onInteractiveBoundsChanged?: (event: {
|
|
62
|
+
nativeEvent: {
|
|
63
|
+
selector: string;
|
|
64
|
+
left: number;
|
|
65
|
+
top: number;
|
|
66
|
+
right: number;
|
|
67
|
+
bottom: number;
|
|
68
|
+
};
|
|
69
|
+
}) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const NativePylonChatView =
|
|
73
|
+
requireNativeComponent<NativePylonChatViewProps>("RNPylonChatView");
|
|
74
|
+
|
|
75
|
+
const COMMANDS = {
|
|
76
|
+
openChat: "openChat",
|
|
77
|
+
closeChat: "closeChat",
|
|
78
|
+
showChatBubble: "showChatBubble",
|
|
79
|
+
hideChatBubble: "hideChatBubble",
|
|
80
|
+
showNewMessage: "showNewMessage",
|
|
81
|
+
setNewIssueCustomFields: "setNewIssueCustomFields",
|
|
82
|
+
setTicketFormFields: "setTicketFormFields",
|
|
83
|
+
updateEmailHash: "updateEmailHash",
|
|
84
|
+
showTicketForm: "showTicketForm",
|
|
85
|
+
showKnowledgeBaseArticle: "showKnowledgeBaseArticle",
|
|
86
|
+
clickElementAtSelector: "clickElementAtSelector",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const PylonChatView = React.forwardRef<
|
|
90
|
+
PylonChatViewInternalRef,
|
|
91
|
+
PylonChatViewProps
|
|
92
|
+
>(({ config, user, style, listener, topInset = 0 }, ref) => {
|
|
93
|
+
const nativeRef = useRef(null);
|
|
94
|
+
|
|
95
|
+
const dispatchCommand = (commandName: string, args: any[] = []) => {
|
|
96
|
+
const handle = findNodeHandle(nativeRef.current);
|
|
97
|
+
if (handle) {
|
|
98
|
+
UIManager.dispatchViewManagerCommand(handle, commandName, args);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Expose imperative methods via ref
|
|
103
|
+
useImperativeHandle(
|
|
104
|
+
ref,
|
|
105
|
+
() => ({
|
|
106
|
+
openChat: () => dispatchCommand(COMMANDS.openChat),
|
|
107
|
+
closeChat: () => dispatchCommand(COMMANDS.closeChat),
|
|
108
|
+
showChatBubble: () => dispatchCommand(COMMANDS.showChatBubble),
|
|
109
|
+
hideChatBubble: () => dispatchCommand(COMMANDS.hideChatBubble),
|
|
110
|
+
showNewMessage: (message: string, isHtml = false) =>
|
|
111
|
+
dispatchCommand(COMMANDS.showNewMessage, [message, isHtml]),
|
|
112
|
+
setNewIssueCustomFields: (fields: Record<string, any>) =>
|
|
113
|
+
dispatchCommand(COMMANDS.setNewIssueCustomFields, [fields]),
|
|
114
|
+
setTicketFormFields: (fields: Record<string, any>) =>
|
|
115
|
+
dispatchCommand(COMMANDS.setTicketFormFields, [fields]),
|
|
116
|
+
updateEmailHash: (emailHash: string | null) =>
|
|
117
|
+
dispatchCommand(COMMANDS.updateEmailHash, [emailHash]),
|
|
118
|
+
showTicketForm: (slug: string) =>
|
|
119
|
+
dispatchCommand(COMMANDS.showTicketForm, [slug]),
|
|
120
|
+
showKnowledgeBaseArticle: (articleId: string) =>
|
|
121
|
+
dispatchCommand(COMMANDS.showKnowledgeBaseArticle, [articleId]),
|
|
122
|
+
clickElementAtSelector: (selector: string) =>
|
|
123
|
+
dispatchCommand(COMMANDS.clickElementAtSelector, [selector]),
|
|
124
|
+
}),
|
|
125
|
+
[]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<NativePylonChatView
|
|
130
|
+
ref={nativeRef}
|
|
131
|
+
style={style}
|
|
132
|
+
appId={config.appId}
|
|
133
|
+
widgetBaseUrl={config.widgetBaseUrl}
|
|
134
|
+
widgetScriptUrl={config.widgetScriptUrl}
|
|
135
|
+
enableLogging={config.enableLogging}
|
|
136
|
+
debugMode={config.debugMode}
|
|
137
|
+
primaryColor={config.primaryColor}
|
|
138
|
+
userEmail={user?.email}
|
|
139
|
+
userName={user?.name}
|
|
140
|
+
userAvatarUrl={user?.avatarUrl}
|
|
141
|
+
userEmailHash={user?.emailHash}
|
|
142
|
+
userAccountId={user?.accountId}
|
|
143
|
+
userAccountExternalId={user?.accountExternalId}
|
|
144
|
+
topInset={topInset}
|
|
145
|
+
onPylonLoaded={() => listener?.onPylonLoaded?.()}
|
|
146
|
+
onPylonInitialized={() => listener?.onPylonInitialized?.()}
|
|
147
|
+
onPylonReady={() => listener?.onPylonReady?.()}
|
|
148
|
+
onChatOpened={() => listener?.onChatOpened?.()}
|
|
149
|
+
onChatClosed={(event) =>
|
|
150
|
+
listener?.onChatClosed?.(event.nativeEvent.wasOpen)
|
|
151
|
+
}
|
|
152
|
+
onUnreadCountChanged={(event) =>
|
|
153
|
+
listener?.onUnreadCountChanged?.(event.nativeEvent.count)
|
|
154
|
+
}
|
|
155
|
+
onMessageReceived={(event) =>
|
|
156
|
+
listener?.onMessageReceived?.(event.nativeEvent.message)
|
|
157
|
+
}
|
|
158
|
+
onPylonError={(event) =>
|
|
159
|
+
listener?.onPylonError?.(event.nativeEvent.error)
|
|
160
|
+
}
|
|
161
|
+
onInteractiveBoundsChanged={(event) =>
|
|
162
|
+
listener?.onInteractiveBoundsChanged?.(event.nativeEvent)
|
|
163
|
+
}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
PylonChatView.displayName = "PylonChatView";
|
|
169
|
+
|
|
170
|
+
export default PylonChatView;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { Pressable, StyleSheet, View } from "react-native";
|
|
9
|
+
import type {
|
|
10
|
+
PylonChatViewInternalRef,
|
|
11
|
+
PylonChatViewRef,
|
|
12
|
+
} from "./PylonChatView";
|
|
13
|
+
import { PylonChatView } from "./PylonChatView";
|
|
14
|
+
import type { PylonChatWidgetProps } from "./PylonChatWidget";
|
|
15
|
+
import type { InteractiveBound } from "./types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Android implementation using proxy-based touch pass-through.
|
|
19
|
+
*
|
|
20
|
+
* State Management:
|
|
21
|
+
* - isChatOpen is ONLY set by native events (onChatOpened/onChatClosed)
|
|
22
|
+
* - Imperative methods (openChat/closeChat) call native, which then fires events
|
|
23
|
+
* - This ensures state is always synced with native layer
|
|
24
|
+
*
|
|
25
|
+
* Touch Pass-Through Strategy:
|
|
26
|
+
* 1. Native reports interactive element positions via onInteractiveBoundsChanged
|
|
27
|
+
* 2. React renders clickable Pressable views at those positions
|
|
28
|
+
* 3. When chat is closed: WebView has pointerEvents="none" (passes touches through)
|
|
29
|
+
* 4. Touches hit background OR proxy
|
|
30
|
+
* 5. Proxy clicked → calls openChat() → fires onChatOpened → state updates → WebView enabled
|
|
31
|
+
*/
|
|
32
|
+
export const PylonChatWidget = forwardRef<
|
|
33
|
+
PylonChatViewRef,
|
|
34
|
+
PylonChatWidgetProps
|
|
35
|
+
>(({ config, user, listener, style, topInset }, ref) => {
|
|
36
|
+
// State synced ONLY via native events - single source of truth
|
|
37
|
+
// TODO: Consider useSyncExternalStore for a more resilient solution in the future.
|
|
38
|
+
const [isChatOpen, setIsChatOpen] = useState(false);
|
|
39
|
+
const [interactiveBounds, setInteractiveBounds] = useState<
|
|
40
|
+
InteractiveBound[]
|
|
41
|
+
>([]);
|
|
42
|
+
|
|
43
|
+
// Internal ref has additional methods not exposed to SDK users.
|
|
44
|
+
const chatRef = useRef<PylonChatViewInternalRef>(null);
|
|
45
|
+
|
|
46
|
+
// Forward ref methods - these call native, which fires events that update state
|
|
47
|
+
// CRITICAL: DO NOT set state directly here - let events handle it
|
|
48
|
+
useImperativeHandle(ref, () => ({
|
|
49
|
+
openChat: () => {
|
|
50
|
+
chatRef.current?.openChat();
|
|
51
|
+
},
|
|
52
|
+
closeChat: () => {
|
|
53
|
+
chatRef.current?.closeChat();
|
|
54
|
+
},
|
|
55
|
+
showChatBubble: () => chatRef.current?.showChatBubble(),
|
|
56
|
+
hideChatBubble: () => chatRef.current?.hideChatBubble(),
|
|
57
|
+
showNewMessage: (message: string, isHtml?: boolean) =>
|
|
58
|
+
chatRef.current?.showNewMessage(message, isHtml),
|
|
59
|
+
setNewIssueCustomFields: (fields: Record<string, any>) =>
|
|
60
|
+
chatRef.current?.setNewIssueCustomFields(fields),
|
|
61
|
+
setTicketFormFields: (fields: Record<string, any>) =>
|
|
62
|
+
chatRef.current?.setTicketFormFields(fields),
|
|
63
|
+
updateEmailHash: (emailHash: string | null) =>
|
|
64
|
+
chatRef.current?.updateEmailHash(emailHash),
|
|
65
|
+
showTicketForm: (slug: string) => chatRef.current?.showTicketForm(slug),
|
|
66
|
+
showKnowledgeBaseArticle: (articleId: string) =>
|
|
67
|
+
chatRef.current?.showKnowledgeBaseArticle(articleId),
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// CRITICAL: State is ONLY updated by these event handlers.
|
|
71
|
+
// The flow is: imperative method → native JS call → Pylon widget event → native callback → React event.
|
|
72
|
+
const handleChatOpened = useCallback(() => {
|
|
73
|
+
setIsChatOpen(true);
|
|
74
|
+
listener?.onChatOpened?.();
|
|
75
|
+
}, [listener]);
|
|
76
|
+
|
|
77
|
+
const handleChatClosed = useCallback(
|
|
78
|
+
(wasOpen: boolean) => {
|
|
79
|
+
setIsChatOpen(false);
|
|
80
|
+
listener?.onChatClosed?.(wasOpen);
|
|
81
|
+
},
|
|
82
|
+
[listener]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handleBoundsChanged = useCallback((bounds: InteractiveBound) => {
|
|
86
|
+
setInteractiveBounds((prev) => {
|
|
87
|
+
const existing = prev.findIndex((b) => b.selector === bounds.selector);
|
|
88
|
+
|
|
89
|
+
// If bounds are 0,0,0,0 it means element is hidden - remove it
|
|
90
|
+
const isHidden =
|
|
91
|
+
bounds.left === 0 &&
|
|
92
|
+
bounds.top === 0 &&
|
|
93
|
+
bounds.right === 0 &&
|
|
94
|
+
bounds.bottom === 0;
|
|
95
|
+
|
|
96
|
+
if (existing >= 0) {
|
|
97
|
+
const updated = [...prev];
|
|
98
|
+
if (isHidden) {
|
|
99
|
+
updated.splice(existing, 1);
|
|
100
|
+
} else {
|
|
101
|
+
updated[existing] = bounds;
|
|
102
|
+
}
|
|
103
|
+
return updated;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isHidden) {
|
|
107
|
+
return prev;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [...prev, bounds];
|
|
111
|
+
});
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const handleProxyPress = useCallback((selector: string) => {
|
|
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
|
+
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
<View
|
|
125
|
+
style={[StyleSheet.absoluteFill, style]}
|
|
126
|
+
pointerEvents={isChatOpen ? "auto" : "none"}
|
|
127
|
+
>
|
|
128
|
+
{/* The actual WebView - disabled when chat is closed */}
|
|
129
|
+
<PylonChatView
|
|
130
|
+
ref={chatRef}
|
|
131
|
+
style={StyleSheet.absoluteFillObject}
|
|
132
|
+
config={config}
|
|
133
|
+
user={user}
|
|
134
|
+
topInset={topInset}
|
|
135
|
+
listener={{
|
|
136
|
+
...listener,
|
|
137
|
+
onChatOpened: handleChatOpened,
|
|
138
|
+
onChatClosed: handleChatClosed,
|
|
139
|
+
onInteractiveBoundsChanged: handleBoundsChanged,
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
</View>
|
|
143
|
+
{!isChatOpen &&
|
|
144
|
+
interactiveBounds.map((bounds, index) => (
|
|
145
|
+
<Pressable
|
|
146
|
+
key={`${bounds.selector}-${index}`}
|
|
147
|
+
style={{
|
|
148
|
+
position: "absolute",
|
|
149
|
+
left: bounds.left,
|
|
150
|
+
top: bounds.top,
|
|
151
|
+
width: bounds.right - bounds.left,
|
|
152
|
+
height: bounds.bottom - bounds.top,
|
|
153
|
+
backgroundColor:
|
|
154
|
+
__DEV__ && config.debugMode ? "rgba(0,255,0,0.2)" : undefined,
|
|
155
|
+
borderWidth: __DEV__ && config.debugMode ? 2 : 0,
|
|
156
|
+
borderColor: __DEV__ && config.debugMode ? "cyan" : undefined,
|
|
157
|
+
}}
|
|
158
|
+
onPress={() => handleProxyPress(bounds.selector)}
|
|
159
|
+
/>
|
|
160
|
+
))}
|
|
161
|
+
</>
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
PylonChatWidget.displayName = "PylonChatWidget";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ViewStyle } from "react-native";
|
|
2
|
+
import type { PylonChatViewRef } from "./PylonChatView";
|
|
3
|
+
import type { PylonChatListener, PylonConfig, PylonUser } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface PylonChatWidgetProps {
|
|
6
|
+
config: PylonConfig;
|
|
7
|
+
user?: PylonUser;
|
|
8
|
+
listener?: PylonChatListener;
|
|
9
|
+
style?: ViewStyle;
|
|
10
|
+
topInset?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const PylonChatWidget: React.ForwardRefExoticComponent<
|
|
14
|
+
PylonChatWidgetProps & React.RefAttributes<PylonChatViewRef>
|
|
15
|
+
>;
|