@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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ Все заметные изменения в пакете документируются в этом файле.
4
+ Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
5
+ проект следует [семантическому версионированию](https://semver.org/lang/ru/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0-beta.1]
10
+
11
+ ### Added
12
+ - Первая бета-версия React Native SDK для Chat Platform.
13
+ - `ChatSDK.init()` с авторизацией по токену.
14
+ - Компонент `ChatScreen` и хук `useChat`.
15
+ - Поддержка вложений, кнопок в сообщениях, CSI-опросов.
16
+ - Нативные модули file-picker и downloader (Android/iOS).
17
+ - Сборка пакета через `react-native-builder-bob` (commonjs + module + typescript).
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "ChatPlatformSdk"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = "https://github.com/robotmia/chat-platform"
10
+ s.license = "UNLICENSED"
11
+ s.author = "Chat Platform"
12
+ s.platforms = { :ios => "13.4" }
13
+ s.source = { :git => "https://github.com/robotmia/chat-platform.git", :tag => "#{s.version}" }
14
+
15
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
16
+ s.swift_version = "5.0"
17
+ s.requires_arc = true
18
+
19
+ s.dependency "React-Core"
20
+ end
package/README.md ADDED
@@ -0,0 +1,315 @@
1
+ # @chat-platform/sdk-react-native
2
+
3
+ React Native SDK для встройки чата в мобильное приложение.
4
+
5
+ **Версия:** 0.1.0-beta.1
6
+ **JS-требования:** React Native ≥ 0.72, React ≥ 18.2
7
+ **Нативные минимумы:** iOS 13.4, Android API 24 (Android 7.0)
8
+
9
+ Пикер файлов и скачивание вложений работают «из коробки» — никаких дополнительных
10
+ нативных модулей (`expo-*`, `@react-native-documents/*` и т.п.) ставить **не нужно**.
11
+
12
+ ---
13
+
14
+ ## Установка
15
+
16
+ ```bash
17
+ # npm
18
+ npm install @chat-platform/sdk-react-native
19
+
20
+ # yarn
21
+ yarn add @chat-platform/sdk-react-native
22
+ ```
23
+
24
+ После установки:
25
+
26
+ **iOS**
27
+ ```bash
28
+ cd ios && pod install
29
+ ```
30
+
31
+ **Android** — пересборка через Android Studio или `./gradlew assembleDebug`.
32
+ Никаких правок в `MainApplication.kt` / `AppDelegate.mm` не требуется — модуль
33
+ подключается через autolinking (см. `react-native.config.js` в пакете).
34
+
35
+ ---
36
+
37
+ ## Быстрый старт
38
+
39
+ ### 1. Инициализация (один раз при старте приложения)
40
+
41
+ ```tsx
42
+ // App.tsx
43
+ import { ChatSDK } from '@chat-platform/sdk-react-native'
44
+
45
+ ChatSDK.init({
46
+ token: 'ВАШ_ТОКЕН_ИЗ_ЧП', // универсальный токен (содержит widget token + baseUrl)
47
+ locale: 'ru', // 'ru' | 'en' (опционально)
48
+ })
49
+ ```
50
+
51
+ Токен из настроек виджета в ЧП — это base64-JSON вида `{"token":"...","baseUrl":"..."}`.
52
+ SDK сам распакует его и подставит нужный `baseUrl`. Передавать `baseUrl` отдельно
53
+ не нужно.
54
+
55
+ ### 2. Логин после авторизации пользователя
56
+
57
+ ```tsx
58
+ await ChatSDK.login(
59
+ {
60
+ userId: user.id, // обязательно — id из вашей системы
61
+ name: user.firstName,
62
+ surname: user.lastName,
63
+ email: user.email,
64
+ phone: user.phone,
65
+ },
66
+ {
67
+ platform: 'ios', // 'ios' | 'android' | 'other'
68
+ appVersion: '1.0.0',
69
+ bundleId: 'com.yourapp',
70
+ },
71
+ )
72
+ ```
73
+
74
+ ### 3. Открыть чат
75
+
76
+ ```tsx
77
+ import { ChatScreen } from '@chat-platform/sdk-react-native'
78
+
79
+ // В стеке навигации
80
+ <Stack.Screen name="Chat" component={ChatScreen} />
81
+
82
+ // или как компонент с onClose
83
+ <ChatScreen onClose={() => navigation.goBack()} />
84
+ ```
85
+
86
+ ### 4. Логаут
87
+
88
+ ```tsx
89
+ await ChatSDK.logout()
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Что включено по умолчанию
95
+
96
+ | Возможность | Android | iOS |
97
+ |------------------------------------------|------------------------------------------|--------------------------------------|
98
+ | Выбор файлов из системного пикера | SAF `OpenDocument` (множественный выбор) | `UIDocumentPickerViewController` |
99
+ | Скачивание вложений в системные «Файлы» | `MediaStore.Downloads` (API 29+) | `UIActivityViewController` (share-sheet) |
100
+ | Прогресс скачивания | Через `NativeEventEmitter` | Через `NativeEventEmitter` |
101
+ | Notification «Загрузка завершена» | Да, тап открывает файл | Native share-sheet после загрузки |
102
+ | Realtime (Laravel Reverb / Pusher) | Да | Да |
103
+ | Поддержка кнопок ботов в сообщениях | Да | Да |
104
+ | CSI-опрос после закрытия диалога | Да | Да |
105
+ | Галерея вложений (изображения + документы) | Да | Да |
106
+
107
+ Никаких runtime-permissions для скачивания на Android 10+ запрашивать не нужно.
108
+
109
+ ---
110
+
111
+ ## Компонент `<ChatScreen />`
112
+
113
+ | Prop | Тип | Назначение |
114
+ |-------------------------|----------------------------------------------|---------------------------------------------------------|
115
+ | `onClose` | `() => void` | Колбэк для кнопки «назад» в хедере |
116
+ | `theme` | `Partial<ChatTheme>` | Переопределение цветов поверх темы из ЧП |
117
+ | `strings` | `ChatStrings` | Кастомные тексты (см. ниже) |
118
+ | `onPickFiles` | `() => Promise<AttachmentInput[] \| null>` | Escape-hatch: своя реализация пикера |
119
+ | `onDownloadAttachment` | `(a: GalleryAttachment) => Promise<void>` | Escape-hatch: своя реализация скачивания |
120
+
121
+ `onPickFiles` и `onDownloadAttachment` нужны **только** если вы хотите перебить
122
+ встроенное поведение (например, открывать кастомный пикер). Во всех остальных
123
+ случаях оставляйте их `undefined` — SDK использует свои нативные модули.
124
+
125
+ ### Кастомные тексты
126
+
127
+ ```tsx
128
+ <ChatScreen
129
+ strings={{
130
+ headerTitle: 'Поддержка',
131
+ emptyStateText: 'Опишите проблему — мы ответим в течение минуты',
132
+ inputPlaceholder: 'Ваше сообщение…',
133
+ errorRetry: 'Повторить',
134
+ galleryDownload: 'Сохранить',
135
+ surveyTitle: 'Оцените работу оператора',
136
+ surveySubmit: 'Отправить',
137
+ surveySkip: 'Пропустить',
138
+ surveyClose: 'Закрыть',
139
+ sendingText: 'Отправка…',
140
+ }}
141
+ />
142
+ ```
143
+
144
+ Все поля опциональны — непереданные значения берутся из встроенных дефолтов на
145
+ русском.
146
+
147
+ ### Тема
148
+
149
+ ```tsx
150
+ <ChatScreen
151
+ theme={{
152
+ primaryColor: '#7c3aed',
153
+ headerBg: '#7c3aed',
154
+ outboundBg: '#7c3aed',
155
+ background: '#fafafa',
156
+ }}
157
+ />
158
+ ```
159
+
160
+ Полный список полей — в `ChatTheme` (см. `src/theme.ts`).
161
+
162
+ ---
163
+
164
+ ## События
165
+
166
+ После `ChatSDK.login()` SDK поднимает фоновую сессию (WebSocket + polling fallback)
167
+ и держит её живой до `logout()`. Поэтому события ниже **работают независимо от того,
168
+ открыт ли `<ChatScreen />`** — пока приложение в foreground, подписчик будет получать
169
+ их даже при закрытом чате.
170
+
171
+ ```tsx
172
+ const unsubState = ChatSDK.on('stateChange', (state) => {
173
+ // 'idle' | 'initializing' | 'ready' | 'authenticated' | 'error'
174
+ })
175
+
176
+ const unsubOperator = ChatSDK.on('operatorChanged', (p) => {
177
+ // p: { token, contactId, previousOperator, operator, occurredAt }
178
+ })
179
+
180
+ const unsubMessage = ChatSDK.on('newMessage', (p) => {
181
+ // p: { token, contactId, message, operator, occurredAt }
182
+ // Срабатывает только для сообщений от операторов (type === 'user').
183
+ })
184
+
185
+ const unsubMessagesUpdated = ChatSDK.on('messagesUpdated', (p) => {
186
+ // p: { messages, operator } — полный текущий список после любого refresh
187
+ })
188
+
189
+ const unsubConnected = ChatSDK.on('connectedChange', (online) => {
190
+ // true когда WS подключён, false при отвале (тогда работает polling)
191
+ })
192
+
193
+ const unsubError = ChatSDK.on('error', (err) => {
194
+ // ошибка инициализации/сессии
195
+ })
196
+
197
+ // Снятие подписки
198
+ unsubState(); unsubOperator(); unsubMessage()
199
+ unsubMessagesUpdated(); unsubConnected(); unsubError()
200
+ ```
201
+
202
+ Подписка на `newMessage` подходит для собственных in-app уведомлений (тосты,
203
+ бейджи в табах) — событие приходит даже если пользователь не открывал чат, пока
204
+ приложение в foreground. Для push-уведомлений в background используйте webhook
205
+ (см. ниже).
206
+
207
+ ### Поведение в фоне
208
+
209
+ - При уходе в background polling приостанавливается (батарея).
210
+ - WebSocket OS-зависим: некоторые системы дают ему ещё минуту-две, потом гасят.
211
+ - При возврате в foreground SDK делает one-shot `refreshMessages` и при
212
+ необходимости поднимает сокет.
213
+
214
+ ---
215
+
216
+ ## Push-уведомления
217
+
218
+ ЧП **не** шлёт FCM/APNs напрямую. Вместо этого ЧП отправляет webhook на ваш
219
+ backend при новом сообщении от оператора. Ваш backend сам решает, кому и как
220
+ доставить push.
221
+
222
+ ### Webhook payload
223
+
224
+ ```json
225
+ {
226
+ "event": "message.created",
227
+ "version": 1,
228
+ "token": "widget_token",
229
+ "contactId": "user_123",
230
+ "conversationId": 456,
231
+ "messageId": 789,
232
+ "senderType": "user",
233
+ "preview": "Здравствуйте!",
234
+ "hasAttachments": false,
235
+ "createdAt": "2026-06-01T10:00:00Z"
236
+ }
237
+ ```
238
+
239
+ Подпись: заголовок `X-Chat-Platform-Signature: sha256=HMAC_SHA256(body, webhook_secret)`.
240
+
241
+ ### Открытие чата из тапа по push
242
+
243
+ ```tsx
244
+ import { ChatSDK } from '@chat-platform/sdk-react-native'
245
+
246
+ // В обработчике нотификации
247
+ ChatSDK.handleNotification({
248
+ token: data.cp_token,
249
+ contactId: data.cp_contact_id,
250
+ })
251
+ navigation.navigate('Chat')
252
+ ```
253
+
254
+ Рекомендуемые data-ключи в payload FCM/APNs: `cp_token`, `cp_contact_id`.
255
+
256
+ ---
257
+
258
+ ## API
259
+
260
+ ### `ChatSDK.init(config)`
261
+
262
+ | Поле | Тип | Обязательно |
263
+ |----------|--------------------|-------------|
264
+ | `token` | `string` | Да — универсальный токен из ЧП (base64-JSON с `token` + `baseUrl`) |
265
+ | `baseUrl`| `string` | Нет — указывается только при ручной разработке против локального инстанса ЧП |
266
+ | `locale` | `'ru' \| 'en'` | Нет |
267
+
268
+ ### `ChatSDK.login(user, device?)`
269
+
270
+ | Поле | Тип | Обязательно |
271
+ |------------|-----------|-------------|
272
+ | `userId` | `string` | Да |
273
+ | `name` | `string` | Нет |
274
+ | `surname` | `string` | Нет |
275
+ | `email` | `string` | Нет |
276
+ | `phone` | `string` | Нет |
277
+
278
+ `device` (опционально): `{ platform, appVersion, bundleId }`.
279
+
280
+ ### `ChatSDK.logout()`
281
+
282
+ Завершает сессию, отключает realtime.
283
+
284
+ ### `ChatSDK.handleNotification(payload)`
285
+
286
+ Помечает push как относящийся к нашему виджету (по `token`). Навигацию на
287
+ `ChatScreen` запускает host-приложение.
288
+
289
+ ### `ChatSDK.getState() / isAuthenticated() / getUser()`
290
+
291
+ Геттеры текущего состояния SDK.
292
+
293
+ ### `ChatSDK.on(event, handler)`
294
+
295
+ Подписка на события — см. раздел «События» выше. Возвращает функцию отписки.
296
+
297
+ ---
298
+
299
+ ## Чеклист интеграции
300
+
301
+ - [ ] Получить `widget_token` и `baseUrl` от ЧП
302
+ - [ ] `npm install @chat-platform/sdk-react-native` + `pod install` для iOS
303
+ - [ ] Пересобрать нативную часть (Xcode / Android Studio)
304
+ - [ ] `ChatSDK.init(...)` в точке входа приложения
305
+ - [ ] `ChatSDK.login(...)` после авторизации пользователя
306
+ - [ ] `<ChatScreen />` в навигаторе
307
+ - [ ] Webhook URL в настройках виджета в ЧП
308
+ - [ ] FCM/APNs: данные `cp_token`, `cp_contact_id` в data payload
309
+ - [ ] `ChatSDK.handleNotification(...)` в обработчике push на стороне приложения
310
+
311
+ ---
312
+
313
+ ## Лицензия
314
+
315
+ UNLICENSED. Внутренний пакет — для использования только в рамках проектов ЧП.
@@ -0,0 +1,54 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = { name, defaultValue ->
3
+ rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
4
+ }
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+ dependencies {
10
+ classpath "com.android.tools.build:gradle:8.1.1"
11
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22"
12
+ }
13
+ }
14
+
15
+ apply plugin: "com.android.library"
16
+ apply plugin: "kotlin-android"
17
+
18
+ android {
19
+ namespace "com.chatplatform.sdk"
20
+ compileSdkVersion getExtOrDefault("compileSdkVersion", 34)
21
+
22
+ defaultConfig {
23
+ minSdkVersion getExtOrDefault("minSdkVersion", 24)
24
+ targetSdkVersion getExtOrDefault("targetSdkVersion", 34)
25
+ }
26
+
27
+ compileOptions {
28
+ sourceCompatibility JavaVersion.VERSION_17
29
+ targetCompatibility JavaVersion.VERSION_17
30
+ }
31
+
32
+ kotlinOptions {
33
+ jvmTarget = "17"
34
+ }
35
+
36
+ sourceSets {
37
+ main {
38
+ manifest.srcFile "src/main/AndroidManifest.xml"
39
+ java.srcDirs = ["src/main/java"]
40
+ res.srcDirs = ["src/main/res"]
41
+ }
42
+ }
43
+ }
44
+
45
+ repositories {
46
+ google()
47
+ mavenCentral()
48
+ }
49
+
50
+ dependencies {
51
+ implementation "com.facebook.react:react-android"
52
+ implementation "androidx.core:core-ktx:1.13.1"
53
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
54
+ }
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+
4
+ <uses-permission android:name="android.permission.INTERNET" />
5
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
6
+
7
+ <application>
8
+ <provider
9
+ android:name="androidx.core.content.FileProvider"
10
+ android:authorities="${applicationId}.chatsdk.fileprovider"
11
+ android:exported="false"
12
+ android:grantUriPermissions="true">
13
+ <meta-data
14
+ android:name="android.support.FILE_PROVIDER_PATHS"
15
+ android:resource="@xml/chat_sdk_file_paths" />
16
+ </provider>
17
+ </application>
18
+ </manifest>
@@ -0,0 +1,240 @@
1
+ package com.chatplatform.sdk
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.PendingIntent
6
+ import android.content.ContentValues
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.net.Uri
10
+ import android.os.Build
11
+ import android.os.Environment
12
+ import android.provider.MediaStore
13
+ import androidx.core.app.NotificationCompat
14
+ import androidx.core.content.FileProvider
15
+ import com.facebook.react.bridge.Arguments
16
+ import com.facebook.react.bridge.Promise
17
+ import com.facebook.react.bridge.ReactApplicationContext
18
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
19
+ import com.facebook.react.bridge.ReactMethod
20
+ import com.facebook.react.bridge.ReadableMap
21
+ import com.facebook.react.bridge.WritableMap
22
+ import com.facebook.react.modules.core.DeviceEventManagerModule
23
+ import okhttp3.OkHttpClient
24
+ import okhttp3.Request
25
+ import java.io.File
26
+ import java.io.FileOutputStream
27
+ import java.io.InputStream
28
+ import java.util.UUID
29
+ import java.util.concurrent.Executors
30
+ import java.util.concurrent.TimeUnit
31
+
32
+ class ChatSdkDownloaderModule(reactContext: ReactApplicationContext) :
33
+ ReactContextBaseJavaModule(reactContext) {
34
+
35
+ override fun getName(): String = NAME
36
+
37
+ private val executor = Executors.newCachedThreadPool()
38
+ private val client: OkHttpClient by lazy {
39
+ OkHttpClient.Builder()
40
+ .connectTimeout(30, TimeUnit.SECONDS)
41
+ .readTimeout(120, TimeUnit.SECONDS)
42
+ .build()
43
+ }
44
+
45
+ @ReactMethod
46
+ fun addListener(eventName: String) {
47
+ // RN требует наличия этих методов для NativeEventEmitter
48
+ }
49
+
50
+ @ReactMethod
51
+ fun removeListeners(count: Int) {
52
+ // RN требует наличия этих методов для NativeEventEmitter
53
+ }
54
+
55
+ @ReactMethod
56
+ fun download(request: ReadableMap, promise: Promise) {
57
+ val url = request.getString("url")
58
+ val filename = sanitize(request.getString("filename") ?: "file")
59
+ val mime = request.getString("mime") ?: "application/octet-stream"
60
+ if (url.isNullOrEmpty()) {
61
+ promise.reject("INVALID_URL", "URL не задан")
62
+ return
63
+ }
64
+ val headers: ReadableMap? = if (request.hasKey("headers")) request.getMap("headers") else null
65
+ val id = UUID.randomUUID().toString()
66
+
67
+ executor.execute {
68
+ try {
69
+ val builder = Request.Builder().url(url).get()
70
+ headers?.toHashMap()?.forEach { (k, v) ->
71
+ if (v is String) builder.header(k, v)
72
+ }
73
+ val response = client.newCall(builder.build()).execute()
74
+ if (!response.isSuccessful) {
75
+ response.close()
76
+ promise.reject("HTTP_${response.code}", "Ошибка загрузки: HTTP ${response.code}")
77
+ return@execute
78
+ }
79
+
80
+ val body = response.body
81
+ if (body == null) {
82
+ response.close()
83
+ promise.reject("EMPTY_BODY", "Пустой ответ сервера")
84
+ return@execute
85
+ }
86
+
87
+ val total = body.contentLength()
88
+ val context = reactApplicationContext
89
+ val savedUri: Uri = body.byteStream().use { stream ->
90
+ saveToDownloads(context, stream, filename, mime, total, id)
91
+ }
92
+ response.close()
93
+
94
+ postCompletionNotification(context, filename, mime, savedUri)
95
+
96
+ val result: WritableMap = Arguments.createMap()
97
+ result.putString("id", id)
98
+ result.putString("uri", savedUri.toString())
99
+ promise.resolve(result)
100
+ } catch (e: Throwable) {
101
+ promise.reject("DOWNLOAD_FAILED", e.message ?: "Не удалось скачать файл", e)
102
+ }
103
+ }
104
+ }
105
+
106
+ private fun saveToDownloads(
107
+ context: Context,
108
+ input: InputStream,
109
+ filename: String,
110
+ mime: String,
111
+ total: Long,
112
+ id: String,
113
+ ): Uri {
114
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
115
+ saveViaMediaStore(context, input, filename, mime, total, id)
116
+ } else {
117
+ saveToExternalFiles(context, input, filename, total, id)
118
+ }
119
+ }
120
+
121
+ private fun saveViaMediaStore(
122
+ context: Context,
123
+ input: InputStream,
124
+ filename: String,
125
+ mime: String,
126
+ total: Long,
127
+ id: String,
128
+ ): Uri {
129
+ val resolver = context.contentResolver
130
+ val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
131
+ val values = ContentValues().apply {
132
+ put(MediaStore.Downloads.DISPLAY_NAME, filename)
133
+ put(MediaStore.Downloads.MIME_TYPE, mime)
134
+ put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
135
+ put(MediaStore.Downloads.IS_PENDING, 1)
136
+ }
137
+ val uri = resolver.insert(collection, values)
138
+ ?: throw IllegalStateException("Не удалось создать запись в MediaStore")
139
+
140
+ resolver.openOutputStream(uri).use { output ->
141
+ if (output == null) throw IllegalStateException("Не удалось открыть OutputStream")
142
+ copyWithProgress(input, output, total, id)
143
+ }
144
+
145
+ values.clear()
146
+ values.put(MediaStore.Downloads.IS_PENDING, 0)
147
+ resolver.update(uri, values, null, null)
148
+ return uri
149
+ }
150
+
151
+ private fun saveToExternalFiles(
152
+ context: Context,
153
+ input: InputStream,
154
+ filename: String,
155
+ total: Long,
156
+ id: String,
157
+ ): Uri {
158
+ val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
159
+ ?: File(context.filesDir, "downloads").apply { mkdirs() }
160
+ if (!dir.exists()) dir.mkdirs()
161
+ val target = File(dir, filename)
162
+ FileOutputStream(target).use { output -> copyWithProgress(input, output, total, id) }
163
+ val authority = "${context.packageName}.chatsdk.fileprovider"
164
+ return FileProvider.getUriForFile(context, authority, target)
165
+ }
166
+
167
+ private fun copyWithProgress(input: InputStream, output: java.io.OutputStream, total: Long, id: String) {
168
+ val buffer = ByteArray(64 * 1024)
169
+ var written = 0L
170
+ var lastEmit = 0L
171
+ while (true) {
172
+ val read = input.read(buffer)
173
+ if (read <= 0) break
174
+ output.write(buffer, 0, read)
175
+ written += read
176
+ val now = System.currentTimeMillis()
177
+ if (now - lastEmit >= 100) {
178
+ emitProgress(id, written, total)
179
+ lastEmit = now
180
+ }
181
+ }
182
+ output.flush()
183
+ emitProgress(id, written, if (total > 0) total else written)
184
+ }
185
+
186
+ private fun emitProgress(id: String, written: Long, total: Long) {
187
+ val map: WritableMap = Arguments.createMap()
188
+ map.putString("id", id)
189
+ map.putDouble("bytesWritten", written.toDouble())
190
+ map.putDouble("totalBytes", total.toDouble())
191
+ reactApplicationContext
192
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
193
+ .emit("ChatSdkDownloadProgress", map)
194
+ }
195
+
196
+ private fun postCompletionNotification(context: Context, filename: String, mime: String, uri: Uri) {
197
+ ensureChannel(context)
198
+ val openIntent = Intent(Intent.ACTION_VIEW).apply {
199
+ setDataAndType(uri, mime)
200
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
201
+ }
202
+ val pi = PendingIntent.getActivity(
203
+ context,
204
+ uri.hashCode(),
205
+ openIntent,
206
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
207
+ )
208
+ val notification = NotificationCompat.Builder(context, CHANNEL_ID)
209
+ .setSmallIcon(android.R.drawable.stat_sys_download_done)
210
+ .setContentTitle(filename)
211
+ .setContentText("Загрузка завершена")
212
+ .setAutoCancel(true)
213
+ .setContentIntent(pi)
214
+ .build()
215
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
216
+ manager.notify(uri.hashCode(), notification)
217
+ }
218
+
219
+ private fun ensureChannel(context: Context) {
220
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
221
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
222
+ if (manager.getNotificationChannel(CHANNEL_ID) != null) return
223
+ val channel = NotificationChannel(
224
+ CHANNEL_ID,
225
+ "Загрузки",
226
+ NotificationManager.IMPORTANCE_LOW,
227
+ )
228
+ manager.createNotificationChannel(channel)
229
+ }
230
+
231
+ private fun sanitize(name: String): String {
232
+ val cleaned = name.replace(Regex("[/\\\\?%*:|\"<>]"), "_").trim()
233
+ return if (cleaned.isEmpty()) "file" else cleaned
234
+ }
235
+
236
+ companion object {
237
+ const val NAME = "ChatSdkDownloader"
238
+ private const val CHANNEL_ID = "chat_sdk_downloads"
239
+ }
240
+ }