@ion299/sdk-react-native 0.1.0-beta.1
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/CHANGELOG.md +17 -0
- package/ChatPlatformSdk.podspec +20 -0
- package/README.md +315 -0
- package/android/build.gradle +54 -0
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkDownloaderModule.kt +240 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkFilePickerModule.kt +165 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +15 -0
- package/android/src/main/res/xml/chat_sdk_file_paths.xml +7 -0
- package/ios/ChatSdkDownloader.m +10 -0
- package/ios/ChatSdkDownloader.swift +141 -0
- package/ios/ChatSdkFilePicker.m +9 -0
- package/ios/ChatSdkFilePicker.swift +161 -0
- package/lib/commonjs/ChatSDK.js +193 -0
- package/lib/commonjs/ChatSDK.js.map +1 -0
- package/lib/commonjs/api.js +195 -0
- package/lib/commonjs/api.js.map +1 -0
- package/lib/commonjs/attachmentUtils.js +39 -0
- package/lib/commonjs/attachmentUtils.js.map +1 -0
- package/lib/commonjs/components/AttachmentGallery.js +367 -0
- package/lib/commonjs/components/AttachmentGallery.js.map +1 -0
- package/lib/commonjs/components/ChatScreen.js +286 -0
- package/lib/commonjs/components/ChatScreen.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +227 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -0
- package/lib/commonjs/components/MessageInput.js +273 -0
- package/lib/commonjs/components/MessageInput.js.map +1 -0
- package/lib/commonjs/components/SurveyOverlay.js +499 -0
- package/lib/commonjs/components/SurveyOverlay.js.map +1 -0
- package/lib/commonjs/downloaders/defaultAttachmentDownloader.js +28 -0
- package/lib/commonjs/downloaders/defaultAttachmentDownloader.js.map +1 -0
- package/lib/commonjs/filePicker.js +25 -0
- package/lib/commonjs/filePicker.js.map +1 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native/NativeChatSdkDownloader.js +28 -0
- package/lib/commonjs/native/NativeChatSdkDownloader.js.map +1 -0
- package/lib/commonjs/native/NativeChatSdkFilePicker.js +17 -0
- package/lib/commonjs/native/NativeChatSdkFilePicker.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/realtime.js +242 -0
- package/lib/commonjs/realtime.js.map +1 -0
- package/lib/commonjs/safeArea.js +18 -0
- package/lib/commonjs/safeArea.js.map +1 -0
- package/lib/commonjs/session.js +159 -0
- package/lib/commonjs/session.js.map +1 -0
- package/lib/commonjs/surveyCache.js +30 -0
- package/lib/commonjs/surveyCache.js.map +1 -0
- package/lib/commonjs/theme.js +29 -0
- package/lib/commonjs/theme.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useChat.js +145 -0
- package/lib/commonjs/useChat.js.map +1 -0
- package/lib/module/ChatSDK.js +189 -0
- package/lib/module/ChatSDK.js.map +1 -0
- package/lib/module/api.js +190 -0
- package/lib/module/api.js.map +1 -0
- package/lib/module/attachmentUtils.js +33 -0
- package/lib/module/attachmentUtils.js.map +1 -0
- package/lib/module/components/AttachmentGallery.js +362 -0
- package/lib/module/components/AttachmentGallery.js.map +1 -0
- package/lib/module/components/ChatScreen.js +281 -0
- package/lib/module/components/ChatScreen.js.map +1 -0
- package/lib/module/components/MessageBubble.js +222 -0
- package/lib/module/components/MessageBubble.js.map +1 -0
- package/lib/module/components/MessageInput.js +268 -0
- package/lib/module/components/MessageInput.js.map +1 -0
- package/lib/module/components/SurveyOverlay.js +494 -0
- package/lib/module/components/SurveyOverlay.js.map +1 -0
- package/lib/module/downloaders/defaultAttachmentDownloader.js +22 -0
- package/lib/module/downloaders/defaultAttachmentDownloader.js.map +1 -0
- package/lib/module/filePicker.js +20 -0
- package/lib/module/filePicker.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/NativeChatSdkDownloader.js +23 -0
- package/lib/module/native/NativeChatSdkDownloader.js.map +1 -0
- package/lib/module/native/NativeChatSdkFilePicker.js +13 -0
- package/lib/module/native/NativeChatSdkFilePicker.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/realtime.js +236 -0
- package/lib/module/realtime.js.map +1 -0
- package/lib/module/safeArea.js +14 -0
- package/lib/module/safeArea.js.map +1 -0
- package/lib/module/session.js +154 -0
- package/lib/module/session.js.map +1 -0
- package/lib/module/surveyCache.js +23 -0
- package/lib/module/surveyCache.js.map +1 -0
- package/lib/module/theme.js +25 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useChat.js +141 -0
- package/lib/module/useChat.js.map +1 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts +49 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -0
- package/lib/typescript/commonjs/api.d.ts +31 -0
- package/lib/typescript/commonjs/api.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachmentUtils.d.ts +12 -0
- package/lib/typescript/commonjs/attachmentUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/AttachmentGallery.d.ts +16 -0
- package/lib/typescript/commonjs/components/AttachmentGallery.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ChatScreen.d.ts +16 -0
- package/lib/typescript/commonjs/components/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts +12 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageInput.d.ts +14 -0
- package/lib/typescript/commonjs/components/MessageInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/SurveyOverlay.d.ts +13 -0
- package/lib/typescript/commonjs/components/SurveyOverlay.d.ts.map +1 -0
- package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts +3 -0
- package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
- package/lib/typescript/commonjs/filePicker.d.ts +4 -0
- package/lib/typescript/commonjs/filePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +7 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts +24 -0
- package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts +17 -0
- package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/realtime.d.ts +42 -0
- package/lib/typescript/commonjs/realtime.d.ts.map +1 -0
- package/lib/typescript/commonjs/safeArea.d.ts +4 -0
- package/lib/typescript/commonjs/safeArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/session.d.ts +45 -0
- package/lib/typescript/commonjs/session.d.ts.map +1 -0
- package/lib/typescript/commonjs/surveyCache.d.ts +5 -0
- package/lib/typescript/commonjs/surveyCache.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme.d.ts +21 -0
- package/lib/typescript/commonjs/theme.d.ts.map +1 -0
- package/lib/typescript/commonjs/types.d.ts +156 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/useChat.d.ts +16 -0
- package/lib/typescript/commonjs/useChat.d.ts.map +1 -0
- package/lib/typescript/module/ChatSDK.d.ts +49 -0
- package/lib/typescript/module/ChatSDK.d.ts.map +1 -0
- package/lib/typescript/module/api.d.ts +31 -0
- package/lib/typescript/module/api.d.ts.map +1 -0
- package/lib/typescript/module/attachmentUtils.d.ts +12 -0
- package/lib/typescript/module/attachmentUtils.d.ts.map +1 -0
- package/lib/typescript/module/components/AttachmentGallery.d.ts +16 -0
- package/lib/typescript/module/components/AttachmentGallery.d.ts.map +1 -0
- package/lib/typescript/module/components/ChatScreen.d.ts +16 -0
- package/lib/typescript/module/components/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageBubble.d.ts +12 -0
- package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageInput.d.ts +14 -0
- package/lib/typescript/module/components/MessageInput.d.ts.map +1 -0
- package/lib/typescript/module/components/SurveyOverlay.d.ts +13 -0
- package/lib/typescript/module/components/SurveyOverlay.d.ts.map +1 -0
- package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts +3 -0
- package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
- package/lib/typescript/module/filePicker.d.ts +4 -0
- package/lib/typescript/module/filePicker.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +7 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts +24 -0
- package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts.map +1 -0
- package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts +17 -0
- package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/realtime.d.ts +42 -0
- package/lib/typescript/module/realtime.d.ts.map +1 -0
- package/lib/typescript/module/safeArea.d.ts +4 -0
- package/lib/typescript/module/safeArea.d.ts.map +1 -0
- package/lib/typescript/module/session.d.ts +45 -0
- package/lib/typescript/module/session.d.ts.map +1 -0
- package/lib/typescript/module/surveyCache.d.ts +5 -0
- package/lib/typescript/module/surveyCache.d.ts.map +1 -0
- package/lib/typescript/module/theme.d.ts +21 -0
- package/lib/typescript/module/theme.d.ts.map +1 -0
- package/lib/typescript/module/types.d.ts +156 -0
- package/lib/typescript/module/types.d.ts.map +1 -0
- package/lib/typescript/module/useChat.d.ts +16 -0
- package/lib/typescript/module/useChat.d.ts.map +1 -0
- package/package.json +75 -0
- package/react-native.config.js +10 -0
- package/src/ChatSDK.ts +237 -0
- package/src/api.ts +228 -0
- package/src/attachmentUtils.ts +49 -0
- package/src/components/AttachmentGallery.tsx +363 -0
- package/src/components/ChatScreen.tsx +267 -0
- package/src/components/MessageBubble.tsx +208 -0
- package/src/components/MessageInput.tsx +280 -0
- package/src/components/SurveyOverlay.tsx +469 -0
- package/src/downloaders/defaultAttachmentDownloader.ts +27 -0
- package/src/filePicker.ts +22 -0
- package/src/index.ts +30 -0
- package/src/native/NativeChatSdkDownloader.ts +49 -0
- package/src/native/NativeChatSdkFilePicker.ts +30 -0
- package/src/realtime.ts +278 -0
- package/src/safeArea.ts +8 -0
- package/src/session.ts +196 -0
- package/src/surveyCache.ts +24 -0
- package/src/theme.ts +49 -0
- package/src/types.ts +199 -0
- package/src/useChat.ts +190 -0
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ion299/sdk-react-native",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "React Native SDK for Chat Platform",
|
|
5
|
+
"main": "./lib/commonjs/index.js",
|
|
6
|
+
"module": "./lib/module/index.js",
|
|
7
|
+
"types": "./lib/typescript/commonjs/index.d.ts",
|
|
8
|
+
"react-native": "src/index.ts",
|
|
9
|
+
"source": "src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./lib/typescript/module/index.d.ts",
|
|
14
|
+
"default": "./lib/module/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./lib/typescript/commonjs/index.d.ts",
|
|
18
|
+
"default": "./lib/commonjs/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"lib",
|
|
26
|
+
"android",
|
|
27
|
+
"ios",
|
|
28
|
+
"ChatPlatformSdk.podspec",
|
|
29
|
+
"react-native.config.js",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"!**/__tests__",
|
|
33
|
+
"!**/*.test.ts",
|
|
34
|
+
"!**/*.test.tsx"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"clean": "del-cli lib",
|
|
39
|
+
"build": "bob build",
|
|
40
|
+
"prepare": "bob build"
|
|
41
|
+
},
|
|
42
|
+
"react-native-builder-bob": {
|
|
43
|
+
"source": "src",
|
|
44
|
+
"output": "lib",
|
|
45
|
+
"targets": [
|
|
46
|
+
["commonjs", { "esm": true }],
|
|
47
|
+
["module", { "esm": true }],
|
|
48
|
+
["typescript", { "project": "tsconfig.build.json" }]
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": ">=18.2.0",
|
|
53
|
+
"react-native": ">=0.72.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"typescript": "^5.0.0",
|
|
57
|
+
"@types/react": "^18.2.0",
|
|
58
|
+
"@types/react-native": "^0.72.0",
|
|
59
|
+
"react-native-builder-bob": "^0.40.0",
|
|
60
|
+
"del-cli": "^5.1.0"
|
|
61
|
+
},
|
|
62
|
+
"keywords": ["chat", "sdk", "react-native"],
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/YOUR_GITHUB_USERNAME/chat-platform-sdk-rn-test.git"
|
|
66
|
+
},
|
|
67
|
+
"homepage": "https://github.com/YOUR_GITHUB_USERNAME/chat-platform-sdk-rn-test#readme",
|
|
68
|
+
"bugs": {
|
|
69
|
+
"url": "https://github.com/YOUR_GITHUB_USERNAME/chat-platform-sdk-rn-test/issues"
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"license": "UNLICENSED"
|
|
75
|
+
}
|
package/src/ChatSDK.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { MobileApiClient } from './api'
|
|
2
|
+
import { ChatSDKSession } from './session'
|
|
3
|
+
import type {
|
|
4
|
+
ChatMessage,
|
|
5
|
+
ChatOperator,
|
|
6
|
+
ChatSDKConfig,
|
|
7
|
+
ChatSDKDevice,
|
|
8
|
+
ChatSDKUser,
|
|
9
|
+
MessagesUpdatedEventPayload,
|
|
10
|
+
MobileConfig,
|
|
11
|
+
NewMessageEventPayload,
|
|
12
|
+
NotificationPayload,
|
|
13
|
+
OperatorChangedEventPayload,
|
|
14
|
+
SDKState,
|
|
15
|
+
} from './types'
|
|
16
|
+
|
|
17
|
+
type SDKEventMap = {
|
|
18
|
+
stateChange: SDKState
|
|
19
|
+
error: unknown
|
|
20
|
+
operatorChanged: OperatorChangedEventPayload
|
|
21
|
+
newMessage: NewMessageEventPayload
|
|
22
|
+
messagesUpdated: MessagesUpdatedEventPayload
|
|
23
|
+
connectedChange: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type EventName = keyof SDKEventMap
|
|
27
|
+
type EventHandler<K extends EventName = EventName> = (data: SDKEventMap[K]) => void
|
|
28
|
+
|
|
29
|
+
class ChatSDKSingleton {
|
|
30
|
+
private config: ChatSDKConfig | null = null
|
|
31
|
+
private api: MobileApiClient | null = null
|
|
32
|
+
private sdkConfig: MobileConfig | null = null
|
|
33
|
+
private session: ChatSDKSession | null = null
|
|
34
|
+
private state: SDKState = 'idle'
|
|
35
|
+
private lastError: string | null = null
|
|
36
|
+
private currentUser: ChatSDKUser | null = null
|
|
37
|
+
private listeners: Map<EventName, Set<EventHandler>> = new Map()
|
|
38
|
+
|
|
39
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
init(raw: ChatSDKConfig): void {
|
|
42
|
+
// После hot reload в Expo состояние может остаться error — разрешаем переинициализацию
|
|
43
|
+
if (this.state !== 'idle' && this.state !== 'error') {
|
|
44
|
+
console.warn('ChatSDK: already initialized')
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const config = ChatSDKSingleton.resolveConfig(raw)
|
|
49
|
+
this.config = config
|
|
50
|
+
this.api = new MobileApiClient(config.baseUrl!, config.token)
|
|
51
|
+
this.lastError = null
|
|
52
|
+
this.setState('ready')
|
|
53
|
+
|
|
54
|
+
// Конфиг для темы подгружаем в фоне; полный config всё равно приходит из POST /session при login
|
|
55
|
+
void this.api
|
|
56
|
+
.getConfig()
|
|
57
|
+
.then((cfg) => {
|
|
58
|
+
this.sdkConfig = cfg
|
|
59
|
+
if (this.state === 'ready') {
|
|
60
|
+
this.emit('stateChange', 'ready')
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.catch((err) => {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
65
|
+
console.warn('ChatSDK: getConfig failed (non-fatal):', msg)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getLastError(): string | null {
|
|
70
|
+
return this.lastError
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async login(user: ChatSDKUser, device?: ChatSDKDevice): Promise<void> {
|
|
74
|
+
this.assertInitialized()
|
|
75
|
+
const api = this.api!
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await api.createSession(
|
|
79
|
+
user.userId,
|
|
80
|
+
{
|
|
81
|
+
name: user.name,
|
|
82
|
+
surname: user.surname,
|
|
83
|
+
email: user.email,
|
|
84
|
+
phone: user.phone,
|
|
85
|
+
},
|
|
86
|
+
device ?? {},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
api.setSession(result.sessionToken, result.contactId)
|
|
90
|
+
api.setUserProfile({
|
|
91
|
+
name: user.name,
|
|
92
|
+
surname: user.surname,
|
|
93
|
+
email: user.email,
|
|
94
|
+
phone: user.phone,
|
|
95
|
+
})
|
|
96
|
+
this.sdkConfig = result.config
|
|
97
|
+
this.currentUser = user
|
|
98
|
+
this.lastError = null
|
|
99
|
+
|
|
100
|
+
this.startSession()
|
|
101
|
+
this.setState('authenticated')
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
104
|
+
this.lastError = msg
|
|
105
|
+
this.setState('error')
|
|
106
|
+
this.emit('error', err)
|
|
107
|
+
throw err
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async logout(): Promise<void> {
|
|
112
|
+
this.currentUser = null
|
|
113
|
+
this.session?.destroy()
|
|
114
|
+
this.session = null
|
|
115
|
+
this.api?.clearSession()
|
|
116
|
+
this.setState(this.sdkConfig ? 'ready' : 'idle')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Push ─────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
handleNotification(_payload: NotificationPayload): void {
|
|
122
|
+
// Навигация обрабатывается в host app через onNotification callback.
|
|
123
|
+
// SDK только проверяет, что payload относится к нашему токену.
|
|
124
|
+
if (_payload.token !== this.config?.token) return
|
|
125
|
+
// В будущем: открыть ChatScreen если приложение foreground
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Getters ──────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
getApi(): MobileApiClient {
|
|
131
|
+
this.assertInitialized()
|
|
132
|
+
return this.api!
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getSDKConfig(): MobileConfig | null {
|
|
136
|
+
return this.sdkConfig
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getBaseUrl(): string {
|
|
140
|
+
return this.config?.baseUrl ?? ''
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getState(): SDKState {
|
|
144
|
+
return this.state
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
isAuthenticated(): boolean {
|
|
148
|
+
return this.state === 'authenticated'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getUser(): ChatSDKUser | null {
|
|
152
|
+
return this.currentUser
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Кэш последнего ответа /messages. Пустой массив до первой загрузки. */
|
|
156
|
+
getMessages(): ChatMessage[] {
|
|
157
|
+
return this.session?.getMessages() ?? []
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getOperator(): ChatOperator | null {
|
|
161
|
+
return this.session?.getOperator() ?? null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
isRealtimeConnected(): boolean {
|
|
165
|
+
return this.session?.isConnected() ?? false
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Принудительный refresh с сервера. Не бросает при сетевых ошибках после первой загрузки. */
|
|
169
|
+
refreshMessages(): Promise<void> {
|
|
170
|
+
return this.session?.refreshMessages() ?? Promise.resolve()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Events ───────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
on<K extends EventName>(event: K, handler: EventHandler<K>): () => void {
|
|
176
|
+
if (!this.listeners.has(event)) {
|
|
177
|
+
this.listeners.set(event, new Set())
|
|
178
|
+
}
|
|
179
|
+
const handlers = this.listeners.get(event)!
|
|
180
|
+
handlers.add(handler as EventHandler)
|
|
181
|
+
return () => handlers.delete(handler as EventHandler)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
private startSession(): void {
|
|
187
|
+
if (!this.sdkConfig || !this.api || !this.config) return
|
|
188
|
+
this.session?.destroy()
|
|
189
|
+
this.session = new ChatSDKSession(
|
|
190
|
+
this.api,
|
|
191
|
+
this.sdkConfig,
|
|
192
|
+
this.config.token,
|
|
193
|
+
this.config.baseUrl ?? this.sdkConfig.apiBaseUrl,
|
|
194
|
+
{
|
|
195
|
+
emitMessagesUpdated: (messages, operator) =>
|
|
196
|
+
this.emit('messagesUpdated', { messages, operator }),
|
|
197
|
+
emitConnectedChange: (connected) => this.emit('connectedChange', connected),
|
|
198
|
+
emitOperatorChanged: (payload) => this.emit('operatorChanged', payload),
|
|
199
|
+
emitNewMessage: (payload) => this.emit('newMessage', payload),
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
this.session.start()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private setState(next: SDKState): void {
|
|
206
|
+
this.state = next
|
|
207
|
+
this.emit('stateChange', next)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private emit<K extends EventName>(event: K, data: SDKEventMap[K]): void {
|
|
211
|
+
this.listeners.get(event)?.forEach((h) => (h as EventHandler<K>)(data))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private assertInitialized(): void {
|
|
215
|
+
if (!this.api || !this.config) {
|
|
216
|
+
throw new Error('ChatSDK: call init() before login()')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Если token — base64-JSON с полями token+baseUrl, распаковывает его. */
|
|
221
|
+
private static resolveConfig(raw: ChatSDKConfig): ChatSDKConfig & { baseUrl: string } {
|
|
222
|
+
if (!raw.baseUrl) {
|
|
223
|
+
try {
|
|
224
|
+
const decoded = JSON.parse(atob(raw.token)) as { token?: string; baseUrl?: string }
|
|
225
|
+
if (decoded.token && decoded.baseUrl) {
|
|
226
|
+
return { ...raw, token: decoded.token, baseUrl: decoded.baseUrl }
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// не base64 — падём ниже с понятной ошибкой
|
|
230
|
+
}
|
|
231
|
+
throw new Error('ChatSDK: baseUrl is required (or pass a base64-encoded config token)')
|
|
232
|
+
}
|
|
233
|
+
return raw as ChatSDKConfig & { baseUrl: string }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const ChatSDK = new ChatSDKSingleton()
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SessionResponse,
|
|
3
|
+
MobileConfig,
|
|
4
|
+
MessagesResponse,
|
|
5
|
+
ChatSDKDevice,
|
|
6
|
+
SurveyConfigResponse,
|
|
7
|
+
AttachmentInput,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
export class MobileApiClient {
|
|
11
|
+
private baseUrl: string
|
|
12
|
+
private token: string
|
|
13
|
+
private sessionToken: string | null = null
|
|
14
|
+
private contactId: string | null = null
|
|
15
|
+
private userProfile: Record<string, string | undefined> = {}
|
|
16
|
+
|
|
17
|
+
constructor(baseUrl: string, token: string) {
|
|
18
|
+
this.baseUrl = baseUrl.replace(/\/$/, '')
|
|
19
|
+
this.token = token
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setSession(sessionToken: string, contactId: string): void {
|
|
23
|
+
this.sessionToken = sessionToken
|
|
24
|
+
this.contactId = contactId
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setUserProfile(profile: Record<string, string | undefined>): void {
|
|
28
|
+
this.userProfile = profile
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
clearSession(): void {
|
|
32
|
+
this.sessionToken = null
|
|
33
|
+
this.contactId = null
|
|
34
|
+
this.userProfile = {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getContactId(): string | null {
|
|
38
|
+
return this.contactId
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async createSession(
|
|
42
|
+
userId: string,
|
|
43
|
+
profile: Record<string, string | undefined>,
|
|
44
|
+
device: ChatSDKDevice,
|
|
45
|
+
): Promise<SessionResponse> {
|
|
46
|
+
const body = { userId, profile, device }
|
|
47
|
+
const path = `/api/mobile/${this.token}/session`
|
|
48
|
+
let lastError: Error | null = null
|
|
49
|
+
|
|
50
|
+
// До 3 попыток — tunnel/LTE иногда рвёт POST
|
|
51
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
const res = await this.post(path, body, 60_000)
|
|
54
|
+
return res as SessionResponse
|
|
55
|
+
} catch (e) {
|
|
56
|
+
lastError = e instanceof Error ? e : new Error(String(e))
|
|
57
|
+
if (attempt < 2) {
|
|
58
|
+
await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw lastError ?? new Error('Session request failed')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getConfig(): Promise<MobileConfig> {
|
|
67
|
+
const res = await this.get(`/api/mobile/${this.token}/config`)
|
|
68
|
+
return res as MobileConfig
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getMessages(): Promise<MessagesResponse> {
|
|
72
|
+
this.assertSession()
|
|
73
|
+
const res = await this.get(
|
|
74
|
+
`/api/mobile/${this.token}/contact/${this.contactId}/messages`,
|
|
75
|
+
)
|
|
76
|
+
return res as MessagesResponse
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async startDialog(userData: Record<string, string | undefined>): Promise<void> {
|
|
80
|
+
this.assertSession()
|
|
81
|
+
await this.post(
|
|
82
|
+
`/api/mobile/${this.token}/contact/${this.contactId}/start`,
|
|
83
|
+
userData,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async sendMessage(text: string, files?: AttachmentInput[]): Promise<void> {
|
|
88
|
+
this.assertSession()
|
|
89
|
+
const url = `${this.baseUrl}/api/mobile/${this.token}/contact/${this.contactId}/messages`
|
|
90
|
+
|
|
91
|
+
const form = new FormData()
|
|
92
|
+
if (text.trim()) form.append('text', text)
|
|
93
|
+
|
|
94
|
+
if (files && files.length > 0) {
|
|
95
|
+
files.forEach((f) =>
|
|
96
|
+
// React Native FormData принимает объект {uri, name, type} вместо File
|
|
97
|
+
form.append('files[]', { uri: f.uri, name: f.name, type: f.type } as unknown as Blob),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (this.userProfile.name) form.append('name', this.userProfile.name)
|
|
102
|
+
if (this.userProfile.surname) form.append('surname', this.userProfile.surname)
|
|
103
|
+
if (this.userProfile.email) form.append('email', this.userProfile.email)
|
|
104
|
+
if (this.userProfile.phone) form.append('phone', this.userProfile.phone)
|
|
105
|
+
|
|
106
|
+
const res = await fetch(url, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: this.authHeaders(),
|
|
109
|
+
body: form,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const body = await res.json().catch(() => ({}))
|
|
114
|
+
throw new Error(body?.message ?? `HTTP ${res.status}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getSurveyConfig(closeEventCreatedAt?: number): Promise<SurveyConfigResponse> {
|
|
119
|
+
const params = new URLSearchParams()
|
|
120
|
+
if (this.contactId) {
|
|
121
|
+
params.set('contact_id', this.contactId)
|
|
122
|
+
}
|
|
123
|
+
if (closeEventCreatedAt !== undefined) {
|
|
124
|
+
params.set('close_event_at', String(closeEventCreatedAt))
|
|
125
|
+
}
|
|
126
|
+
const query = params.toString()
|
|
127
|
+
const res = await this.get(
|
|
128
|
+
`/api/mobile/${this.token}/survey-config${query ? `?${query}` : ''}`,
|
|
129
|
+
)
|
|
130
|
+
return res as SurveyConfigResponse
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async submitCsi(rating: number, comment?: string): Promise<void> {
|
|
134
|
+
this.assertSession()
|
|
135
|
+
await this.post(
|
|
136
|
+
`/api/mobile/${this.token}/contact/${this.contactId}/csi`,
|
|
137
|
+
{ rating, comment: comment ?? '' },
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async sendCallback(messageId: number, callbackData: string): Promise<void> {
|
|
142
|
+
this.assertSession()
|
|
143
|
+
await this.post(
|
|
144
|
+
`/api/mobile/${this.token}/contact/${this.contactId}/callback`,
|
|
145
|
+
{ message_id: messageId, callback_data: callbackData },
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getDownloadUrl(contactId: string): string {
|
|
150
|
+
return `${this.baseUrl}/api/mobile/${this.token}/contact/${contactId}/download`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
buildAttachmentDownloadUrl(fileUrl: string, filename: string): string {
|
|
154
|
+
this.assertSession()
|
|
155
|
+
const params = new URLSearchParams({
|
|
156
|
+
url: fileUrl,
|
|
157
|
+
filename: filename || 'file',
|
|
158
|
+
})
|
|
159
|
+
return `${this.getDownloadUrl(this.contactId!)}?${params.toString()}`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getBroadcastAuthEndpoint(): string {
|
|
163
|
+
return `${this.baseUrl}/api/mobile/${this.token}/broadcasting/auth`
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getAuthHeaders(): Record<string, string> {
|
|
167
|
+
return this.authHeaders()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private assertSession(): void {
|
|
171
|
+
if (!this.sessionToken || !this.contactId) {
|
|
172
|
+
throw new Error('ChatSDK: not authenticated. Call login() first.')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private authHeaders(): Record<string, string> {
|
|
177
|
+
return this.sessionToken
|
|
178
|
+
? { Authorization: `Bearer ${this.sessionToken}` }
|
|
179
|
+
: {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async get(path: string): Promise<unknown> {
|
|
183
|
+
const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, {
|
|
184
|
+
headers: { Accept: 'application/json', ...this.authHeaders() },
|
|
185
|
+
})
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
const body = await res.json().catch(() => ({}))
|
|
188
|
+
throw new Error(body?.message ?? `HTTP ${res.status}`)
|
|
189
|
+
}
|
|
190
|
+
return res.json()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async post(path: string, body: unknown, timeoutMs = 30_000): Promise<unknown> {
|
|
194
|
+
const res = await this.fetchWithTimeout(
|
|
195
|
+
`${this.baseUrl}${path}`,
|
|
196
|
+
{
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/json',
|
|
200
|
+
Accept: 'application/json',
|
|
201
|
+
...this.authHeaders(),
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify(body),
|
|
204
|
+
},
|
|
205
|
+
timeoutMs,
|
|
206
|
+
)
|
|
207
|
+
if (!res.ok) {
|
|
208
|
+
const json = await res.json().catch(() => ({}))
|
|
209
|
+
throw new Error(json?.message ?? `HTTP ${res.status}`)
|
|
210
|
+
}
|
|
211
|
+
return res.json()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async fetchWithTimeout(url: string, options: RequestInit, timeoutMs = 30000): Promise<Response> {
|
|
215
|
+
const controller = new AbortController()
|
|
216
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
217
|
+
try {
|
|
218
|
+
return await fetch(url, { ...options, signal: controller.signal })
|
|
219
|
+
} catch (e: unknown) {
|
|
220
|
+
if (e instanceof Error && e.name === 'AbortError') {
|
|
221
|
+
throw new Error(`Request timed out: ${url}`)
|
|
222
|
+
}
|
|
223
|
+
throw e
|
|
224
|
+
} finally {
|
|
225
|
+
clearTimeout(timer)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ChatSDK } from './ChatSDK'
|
|
2
|
+
import type { ChatAttachment, GalleryAttachment } from './types'
|
|
3
|
+
|
|
4
|
+
export function attachmentDisplayName(attachment: ChatAttachment): string {
|
|
5
|
+
const name = attachment.filename?.trim()
|
|
6
|
+
if (name) return name
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const path = attachment.url.split('?')[0] ?? ''
|
|
10
|
+
const segment = path.split('/').pop()
|
|
11
|
+
if (segment) return decodeURIComponent(segment)
|
|
12
|
+
} catch {
|
|
13
|
+
// ignore malformed URL
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return 'Файл'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sanitizeFilename(name: string): string {
|
|
20
|
+
return name.replace(/[/\\?%*:|"<>]/g, '_') || 'file'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AttachmentDownloadRequest {
|
|
24
|
+
downloadUrl: string
|
|
25
|
+
filename: string
|
|
26
|
+
headers: Record<string, string>
|
|
27
|
+
mime: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildAttachmentDownloadRequest(
|
|
31
|
+
attachment: GalleryAttachment,
|
|
32
|
+
): AttachmentDownloadRequest {
|
|
33
|
+
const api = ChatSDK.getApi()
|
|
34
|
+
const contactId = api.getContactId()
|
|
35
|
+
if (!contactId) {
|
|
36
|
+
throw new Error('Сессия не активна')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const filename = attachmentDisplayName(attachment)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
downloadUrl: api.buildAttachmentDownloadUrl(attachment.url, filename),
|
|
43
|
+
filename,
|
|
44
|
+
headers: api.getAuthHeaders(),
|
|
45
|
+
mime: attachment.mime || 'application/octet-stream',
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type AttachmentDownloadHandler = (attachment: GalleryAttachment) => Promise<void>
|