@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.
Files changed (199) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/ChatPlatformSdk.podspec +20 -0
  3. package/README.md +315 -0
  4. package/android/build.gradle +54 -0
  5. package/android/src/main/AndroidManifest.xml +18 -0
  6. package/android/src/main/java/com/chatplatform/sdk/ChatSdkDownloaderModule.kt +240 -0
  7. package/android/src/main/java/com/chatplatform/sdk/ChatSdkFilePickerModule.kt +165 -0
  8. package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +15 -0
  9. package/android/src/main/res/xml/chat_sdk_file_paths.xml +7 -0
  10. package/ios/ChatSdkDownloader.m +10 -0
  11. package/ios/ChatSdkDownloader.swift +141 -0
  12. package/ios/ChatSdkFilePicker.m +9 -0
  13. package/ios/ChatSdkFilePicker.swift +161 -0
  14. package/lib/commonjs/ChatSDK.js +193 -0
  15. package/lib/commonjs/ChatSDK.js.map +1 -0
  16. package/lib/commonjs/api.js +195 -0
  17. package/lib/commonjs/api.js.map +1 -0
  18. package/lib/commonjs/attachmentUtils.js +39 -0
  19. package/lib/commonjs/attachmentUtils.js.map +1 -0
  20. package/lib/commonjs/components/AttachmentGallery.js +367 -0
  21. package/lib/commonjs/components/AttachmentGallery.js.map +1 -0
  22. package/lib/commonjs/components/ChatScreen.js +286 -0
  23. package/lib/commonjs/components/ChatScreen.js.map +1 -0
  24. package/lib/commonjs/components/MessageBubble.js +227 -0
  25. package/lib/commonjs/components/MessageBubble.js.map +1 -0
  26. package/lib/commonjs/components/MessageInput.js +273 -0
  27. package/lib/commonjs/components/MessageInput.js.map +1 -0
  28. package/lib/commonjs/components/SurveyOverlay.js +499 -0
  29. package/lib/commonjs/components/SurveyOverlay.js.map +1 -0
  30. package/lib/commonjs/downloaders/defaultAttachmentDownloader.js +28 -0
  31. package/lib/commonjs/downloaders/defaultAttachmentDownloader.js.map +1 -0
  32. package/lib/commonjs/filePicker.js +25 -0
  33. package/lib/commonjs/filePicker.js.map +1 -0
  34. package/lib/commonjs/index.js +27 -0
  35. package/lib/commonjs/index.js.map +1 -0
  36. package/lib/commonjs/native/NativeChatSdkDownloader.js +28 -0
  37. package/lib/commonjs/native/NativeChatSdkDownloader.js.map +1 -0
  38. package/lib/commonjs/native/NativeChatSdkFilePicker.js +17 -0
  39. package/lib/commonjs/native/NativeChatSdkFilePicker.js.map +1 -0
  40. package/lib/commonjs/package.json +1 -0
  41. package/lib/commonjs/realtime.js +242 -0
  42. package/lib/commonjs/realtime.js.map +1 -0
  43. package/lib/commonjs/safeArea.js +18 -0
  44. package/lib/commonjs/safeArea.js.map +1 -0
  45. package/lib/commonjs/session.js +159 -0
  46. package/lib/commonjs/session.js.map +1 -0
  47. package/lib/commonjs/surveyCache.js +30 -0
  48. package/lib/commonjs/surveyCache.js.map +1 -0
  49. package/lib/commonjs/theme.js +29 -0
  50. package/lib/commonjs/theme.js.map +1 -0
  51. package/lib/commonjs/types.js +2 -0
  52. package/lib/commonjs/types.js.map +1 -0
  53. package/lib/commonjs/useChat.js +145 -0
  54. package/lib/commonjs/useChat.js.map +1 -0
  55. package/lib/module/ChatSDK.js +189 -0
  56. package/lib/module/ChatSDK.js.map +1 -0
  57. package/lib/module/api.js +190 -0
  58. package/lib/module/api.js.map +1 -0
  59. package/lib/module/attachmentUtils.js +33 -0
  60. package/lib/module/attachmentUtils.js.map +1 -0
  61. package/lib/module/components/AttachmentGallery.js +362 -0
  62. package/lib/module/components/AttachmentGallery.js.map +1 -0
  63. package/lib/module/components/ChatScreen.js +281 -0
  64. package/lib/module/components/ChatScreen.js.map +1 -0
  65. package/lib/module/components/MessageBubble.js +222 -0
  66. package/lib/module/components/MessageBubble.js.map +1 -0
  67. package/lib/module/components/MessageInput.js +268 -0
  68. package/lib/module/components/MessageInput.js.map +1 -0
  69. package/lib/module/components/SurveyOverlay.js +494 -0
  70. package/lib/module/components/SurveyOverlay.js.map +1 -0
  71. package/lib/module/downloaders/defaultAttachmentDownloader.js +22 -0
  72. package/lib/module/downloaders/defaultAttachmentDownloader.js.map +1 -0
  73. package/lib/module/filePicker.js +20 -0
  74. package/lib/module/filePicker.js.map +1 -0
  75. package/lib/module/index.js +6 -0
  76. package/lib/module/index.js.map +1 -0
  77. package/lib/module/native/NativeChatSdkDownloader.js +23 -0
  78. package/lib/module/native/NativeChatSdkDownloader.js.map +1 -0
  79. package/lib/module/native/NativeChatSdkFilePicker.js +13 -0
  80. package/lib/module/native/NativeChatSdkFilePicker.js.map +1 -0
  81. package/lib/module/package.json +1 -0
  82. package/lib/module/realtime.js +236 -0
  83. package/lib/module/realtime.js.map +1 -0
  84. package/lib/module/safeArea.js +14 -0
  85. package/lib/module/safeArea.js.map +1 -0
  86. package/lib/module/session.js +154 -0
  87. package/lib/module/session.js.map +1 -0
  88. package/lib/module/surveyCache.js +23 -0
  89. package/lib/module/surveyCache.js.map +1 -0
  90. package/lib/module/theme.js +25 -0
  91. package/lib/module/theme.js.map +1 -0
  92. package/lib/module/types.js +2 -0
  93. package/lib/module/types.js.map +1 -0
  94. package/lib/module/useChat.js +141 -0
  95. package/lib/module/useChat.js.map +1 -0
  96. package/lib/typescript/commonjs/ChatSDK.d.ts +49 -0
  97. package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/api.d.ts +31 -0
  99. package/lib/typescript/commonjs/api.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/attachmentUtils.d.ts +12 -0
  101. package/lib/typescript/commonjs/attachmentUtils.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/components/AttachmentGallery.d.ts +16 -0
  103. package/lib/typescript/commonjs/components/AttachmentGallery.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/components/ChatScreen.d.ts +16 -0
  105. package/lib/typescript/commonjs/components/ChatScreen.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/components/MessageBubble.d.ts +12 -0
  107. package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/components/MessageInput.d.ts +14 -0
  109. package/lib/typescript/commonjs/components/MessageInput.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/components/SurveyOverlay.d.ts +13 -0
  111. package/lib/typescript/commonjs/components/SurveyOverlay.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts +3 -0
  113. package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/filePicker.d.ts +4 -0
  115. package/lib/typescript/commonjs/filePicker.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/index.d.ts +7 -0
  117. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts +24 -0
  119. package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts +17 -0
  121. package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/package.json +1 -0
  123. package/lib/typescript/commonjs/realtime.d.ts +42 -0
  124. package/lib/typescript/commonjs/realtime.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/safeArea.d.ts +4 -0
  126. package/lib/typescript/commonjs/safeArea.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/session.d.ts +45 -0
  128. package/lib/typescript/commonjs/session.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/surveyCache.d.ts +5 -0
  130. package/lib/typescript/commonjs/surveyCache.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/theme.d.ts +21 -0
  132. package/lib/typescript/commonjs/theme.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/types.d.ts +156 -0
  134. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/useChat.d.ts +16 -0
  136. package/lib/typescript/commonjs/useChat.d.ts.map +1 -0
  137. package/lib/typescript/module/ChatSDK.d.ts +49 -0
  138. package/lib/typescript/module/ChatSDK.d.ts.map +1 -0
  139. package/lib/typescript/module/api.d.ts +31 -0
  140. package/lib/typescript/module/api.d.ts.map +1 -0
  141. package/lib/typescript/module/attachmentUtils.d.ts +12 -0
  142. package/lib/typescript/module/attachmentUtils.d.ts.map +1 -0
  143. package/lib/typescript/module/components/AttachmentGallery.d.ts +16 -0
  144. package/lib/typescript/module/components/AttachmentGallery.d.ts.map +1 -0
  145. package/lib/typescript/module/components/ChatScreen.d.ts +16 -0
  146. package/lib/typescript/module/components/ChatScreen.d.ts.map +1 -0
  147. package/lib/typescript/module/components/MessageBubble.d.ts +12 -0
  148. package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -0
  149. package/lib/typescript/module/components/MessageInput.d.ts +14 -0
  150. package/lib/typescript/module/components/MessageInput.d.ts.map +1 -0
  151. package/lib/typescript/module/components/SurveyOverlay.d.ts +13 -0
  152. package/lib/typescript/module/components/SurveyOverlay.d.ts.map +1 -0
  153. package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts +3 -0
  154. package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
  155. package/lib/typescript/module/filePicker.d.ts +4 -0
  156. package/lib/typescript/module/filePicker.d.ts.map +1 -0
  157. package/lib/typescript/module/index.d.ts +7 -0
  158. package/lib/typescript/module/index.d.ts.map +1 -0
  159. package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts +24 -0
  160. package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts.map +1 -0
  161. package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts +17 -0
  162. package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts.map +1 -0
  163. package/lib/typescript/module/package.json +1 -0
  164. package/lib/typescript/module/realtime.d.ts +42 -0
  165. package/lib/typescript/module/realtime.d.ts.map +1 -0
  166. package/lib/typescript/module/safeArea.d.ts +4 -0
  167. package/lib/typescript/module/safeArea.d.ts.map +1 -0
  168. package/lib/typescript/module/session.d.ts +45 -0
  169. package/lib/typescript/module/session.d.ts.map +1 -0
  170. package/lib/typescript/module/surveyCache.d.ts +5 -0
  171. package/lib/typescript/module/surveyCache.d.ts.map +1 -0
  172. package/lib/typescript/module/theme.d.ts +21 -0
  173. package/lib/typescript/module/theme.d.ts.map +1 -0
  174. package/lib/typescript/module/types.d.ts +156 -0
  175. package/lib/typescript/module/types.d.ts.map +1 -0
  176. package/lib/typescript/module/useChat.d.ts +16 -0
  177. package/lib/typescript/module/useChat.d.ts.map +1 -0
  178. package/package.json +75 -0
  179. package/react-native.config.js +10 -0
  180. package/src/ChatSDK.ts +237 -0
  181. package/src/api.ts +228 -0
  182. package/src/attachmentUtils.ts +49 -0
  183. package/src/components/AttachmentGallery.tsx +363 -0
  184. package/src/components/ChatScreen.tsx +267 -0
  185. package/src/components/MessageBubble.tsx +208 -0
  186. package/src/components/MessageInput.tsx +280 -0
  187. package/src/components/SurveyOverlay.tsx +469 -0
  188. package/src/downloaders/defaultAttachmentDownloader.ts +27 -0
  189. package/src/filePicker.ts +22 -0
  190. package/src/index.ts +30 -0
  191. package/src/native/NativeChatSdkDownloader.ts +49 -0
  192. package/src/native/NativeChatSdkFilePicker.ts +30 -0
  193. package/src/realtime.ts +278 -0
  194. package/src/safeArea.ts +8 -0
  195. package/src/session.ts +196 -0
  196. package/src/surveyCache.ts +24 -0
  197. package/src/theme.ts +49 -0
  198. package/src/types.ts +199 -0
  199. 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
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ packageInstance: 'new com.chatplatform.sdk.ChatSdkPackage()',
6
+ },
7
+ ios: {},
8
+ },
9
+ },
10
+ }
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>