@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
@@ -0,0 +1,469 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ KeyboardAvoidingView,
6
+ Platform,
7
+ StyleSheet,
8
+ Text,
9
+ TextInput,
10
+ TouchableOpacity,
11
+ TouchableWithoutFeedback,
12
+ View,
13
+ } from 'react-native'
14
+ import type { SurveyConfig, ChatStrings } from '../types'
15
+ import type { ChatTheme } from '../theme'
16
+
17
+ interface Props {
18
+ config: SurveyConfig
19
+ theme: ChatTheme
20
+ strings?: Pick<ChatStrings, 'surveyTitle' | 'surveySubmit' | 'surveySkip' | 'surveyClose' | 'sendingText'>
21
+ onSubmit: (rating: number, comment?: string) => Promise<void>
22
+ onDismiss: () => void
23
+ }
24
+
25
+ type Step = 'rating' | 'comment' | 'result' | 'exit_confirm'
26
+
27
+ function buildRange(from: number, to: number): number[] {
28
+ if (from > to) return []
29
+ return Array.from({ length: to - from + 1 }, (_, i) => from + i)
30
+ }
31
+
32
+ export function SurveyOverlay({ config, theme, strings, onSubmit, onDismiss }: Props) {
33
+ const [step, setStep] = useState<Step>('rating')
34
+ const [rating, setRating] = useState(0)
35
+ const [comment, setComment] = useState('')
36
+ const [resultMessage, setResultMessage] = useState('')
37
+ const [sending, setSending] = useState(false)
38
+
39
+ const backdropAnim = useRef(new Animated.Value(0)).current
40
+ const cardAnim = useRef(new Animated.Value(0.88)).current
41
+ const stepAnim = useRef(new Animated.Value(1)).current
42
+
43
+ const fullRange = buildRange(config.range[0], config.range[1])
44
+ const badRange = buildRange(config.badRange[0], config.badRange[1])
45
+
46
+ const isBadRating = config.badRangeEnabled && badRange.includes(rating)
47
+ const needsComment = config.commentEnabled && (config.badRangeEnabled ? isBadRating : rating > 0)
48
+
49
+ const label = {
50
+ title: strings?.surveyTitle ?? config.title,
51
+ submit: strings?.surveySubmit ?? 'Отправить',
52
+ skip: strings?.surveySkip ?? 'Пропустить',
53
+ close: strings?.surveyClose ?? 'Закрыть',
54
+ sending: strings?.sendingText ?? 'Отправка…',
55
+ }
56
+
57
+ useEffect(() => {
58
+ Animated.parallel([
59
+ Animated.timing(backdropAnim, { toValue: 1, duration: 250, useNativeDriver: true }),
60
+ Animated.spring(cardAnim, {
61
+ toValue: 1,
62
+ damping: 18,
63
+ stiffness: 260,
64
+ useNativeDriver: true,
65
+ }),
66
+ ]).start()
67
+ }, [])
68
+
69
+ const animateStep = useCallback((fn: () => void) => {
70
+ Animated.timing(stepAnim, {
71
+ toValue: 0,
72
+ duration: 160,
73
+ easing: Easing.out(Easing.ease),
74
+ useNativeDriver: true,
75
+ }).start(() => {
76
+ fn()
77
+ Animated.timing(stepAnim, {
78
+ toValue: 1,
79
+ duration: 200,
80
+ easing: Easing.out(Easing.ease),
81
+ useNativeDriver: true,
82
+ }).start()
83
+ })
84
+ }, [stepAnim])
85
+
86
+ const dismiss = useCallback(() => {
87
+ Animated.parallel([
88
+ Animated.timing(backdropAnim, { toValue: 0, duration: 200, useNativeDriver: true }),
89
+ Animated.timing(cardAnim, { toValue: 0.88, duration: 180, useNativeDriver: true }),
90
+ ]).start(() => onDismiss())
91
+ }, [backdropAnim, cardAnim, onDismiss])
92
+
93
+ const handleStarPress = useCallback((score: number) => {
94
+ setRating(score)
95
+ const isBad = config.badRangeEnabled && badRange.includes(score)
96
+ const msg = isBad ? config.badMessage : config.goodMessage
97
+
98
+ if (config.commentEnabled && (config.badRangeEnabled ? isBad : true)) {
99
+ animateStep(() => {
100
+ setResultMessage(msg)
101
+ setStep('comment')
102
+ })
103
+ } else {
104
+ animateStep(() => {
105
+ setResultMessage(msg)
106
+ setStep('result')
107
+ })
108
+ void handleAutoSubmit(score)
109
+ }
110
+ }, [config, badRange, animateStep])
111
+
112
+ const handleAutoSubmit = async (score: number) => {
113
+ try { await onSubmit(score) } catch {}
114
+ }
115
+
116
+ const handleSendComment = useCallback(async () => {
117
+ if (config.commentRequired && !comment.trim()) return
118
+ setSending(true)
119
+ try {
120
+ await onSubmit(rating, comment.trim() || undefined)
121
+ } catch {}
122
+ setSending(false)
123
+ animateStep(() => {
124
+ setResultMessage(config.afterCommentMessage)
125
+ setStep('result')
126
+ })
127
+ }, [rating, comment, config, onSubmit, animateStep])
128
+
129
+ const handleCloseAttempt = useCallback(() => {
130
+ if (step === 'result') { dismiss(); return }
131
+ if (rating > 0) { setStep('exit_confirm') } else { dismiss() }
132
+ }, [step, rating, dismiss])
133
+
134
+ const starCount = fullRange.length
135
+ const starSize = starCount <= 5 ? 44 : starCount <= 7 ? 36 : 28
136
+ const starGap = starCount <= 5 ? 10 : starCount <= 7 ? 7 : 5
137
+
138
+ return (
139
+ <Animated.View style={[styles.backdrop, { opacity: backdropAnim }]}>
140
+ <TouchableWithoutFeedback onPress={handleCloseAttempt}>
141
+ <View style={StyleSheet.absoluteFill} />
142
+ </TouchableWithoutFeedback>
143
+
144
+ <KeyboardAvoidingView
145
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
146
+ style={styles.kav}
147
+ pointerEvents="box-none"
148
+ >
149
+ <Animated.View
150
+ style={[
151
+ styles.card,
152
+ { borderTopColor: theme.primaryColor },
153
+ {
154
+ opacity: cardAnim,
155
+ transform: [
156
+ { scale: cardAnim.interpolate({ inputRange: [0.88, 1], outputRange: [0.88, 1] }) },
157
+ { translateY: cardAnim.interpolate({ inputRange: [0.88, 1], outputRange: [24, 0] }) },
158
+ ],
159
+ },
160
+ ]}
161
+ >
162
+ {/* Кнопка закрытия */}
163
+ <TouchableOpacity style={styles.closeBtn} onPress={handleCloseAttempt} hitSlop={12}>
164
+ <Text style={[styles.closeBtnText, { color: theme.systemText }]}>✕</Text>
165
+ </TouchableOpacity>
166
+
167
+ {/* Заголовок */}
168
+ <Text style={[styles.title, { color: theme.inboundText }]}>{label.title}</Text>
169
+ {!!config.description && (
170
+ <Text style={[styles.description, { color: theme.systemText }]}>{config.description}</Text>
171
+ )}
172
+
173
+ {/* Звёзды */}
174
+ <View style={[styles.starsRow, { gap: starGap }]}>
175
+ {fullRange.map((score) => {
176
+ const filled = score <= rating
177
+ return (
178
+ <TouchableOpacity
179
+ key={score}
180
+ onPress={() => step === 'rating' && handleStarPress(score)}
181
+ activeOpacity={0.75}
182
+ disabled={step !== 'rating'}
183
+ >
184
+ <StarIcon size={starSize} filled={filled} color={theme.primaryColor} />
185
+ </TouchableOpacity>
186
+ )
187
+ })}
188
+ </View>
189
+
190
+ {/* Анимированное содержимое шага */}
191
+ <Animated.View
192
+ style={{
193
+ opacity: stepAnim,
194
+ transform: [{ translateY: stepAnim.interpolate({ inputRange: [0, 1], outputRange: [12, 0] }) }],
195
+ }}
196
+ >
197
+ {step === 'rating' && (
198
+ <TouchableOpacity
199
+ style={styles.skipBtn}
200
+ onPress={dismiss}
201
+ activeOpacity={0.7}
202
+ >
203
+ <Text style={[styles.skipText, { color: theme.systemText }]}>{label.skip}</Text>
204
+ </TouchableOpacity>
205
+ )}
206
+
207
+ {step === 'comment' && (
208
+ <View style={styles.commentSection}>
209
+ {!!resultMessage && (
210
+ <Text style={[styles.resultMsg, { color: theme.primaryColor }]}>{resultMessage}</Text>
211
+ )}
212
+
213
+ <TextInput
214
+ style={[
215
+ styles.commentInput,
216
+ {
217
+ borderColor: theme.inputBorder,
218
+ color: theme.inputText,
219
+ backgroundColor: theme.inputBg,
220
+ },
221
+ ]}
222
+ placeholder="Ваш комментарий..."
223
+ placeholderTextColor={theme.systemText}
224
+ multiline
225
+ numberOfLines={3}
226
+ value={comment}
227
+ onChangeText={setComment}
228
+ editable={!sending}
229
+ textAlignVertical="top"
230
+ />
231
+
232
+ <TouchableOpacity
233
+ style={[
234
+ styles.primaryBtn,
235
+ { backgroundColor: theme.primaryColor },
236
+ (config.commentRequired && !comment.trim() || sending) && styles.btnDisabled,
237
+ ]}
238
+ onPress={handleSendComment}
239
+ disabled={config.commentRequired && !comment.trim() || sending}
240
+ activeOpacity={0.82}
241
+ >
242
+ <Text style={styles.primaryBtnText}>
243
+ {sending ? label.sending : label.submit}
244
+ </Text>
245
+ </TouchableOpacity>
246
+ </View>
247
+ )}
248
+
249
+ {step === 'result' && (
250
+ <View style={styles.resultSection}>
251
+ <Text style={[styles.resultMsg, { color: theme.primaryColor }]}>{resultMessage}</Text>
252
+ <TouchableOpacity
253
+ style={[styles.primaryBtn, { backgroundColor: theme.primaryColor }]}
254
+ onPress={dismiss}
255
+ activeOpacity={0.82}
256
+ >
257
+ <Text style={styles.primaryBtnText}>{label.close}</Text>
258
+ </TouchableOpacity>
259
+ </View>
260
+ )}
261
+ </Animated.View>
262
+
263
+ {/* Подтверждение выхода */}
264
+ {step === 'exit_confirm' && (
265
+ <View style={styles.confirmOverlay}>
266
+ <View style={styles.confirmBox}>
267
+ <Text style={[styles.confirmText, { color: theme.inboundText }]}>
268
+ Вы хотите прекратить сбор обратной связи? Введённая оценка не будет отправлена.
269
+ </Text>
270
+ <View style={styles.confirmBtns}>
271
+ <TouchableOpacity
272
+ style={[styles.confirmSecondary, { borderColor: theme.primaryColor }]}
273
+ onPress={() => setStep(rating > 0 ? (needsComment ? 'comment' : 'result') : 'rating')}
274
+ activeOpacity={0.8}
275
+ >
276
+ <Text style={[styles.confirmSecondaryText, { color: theme.primaryColor }]}>Продолжить</Text>
277
+ </TouchableOpacity>
278
+ <TouchableOpacity
279
+ style={[styles.primaryBtn, { backgroundColor: theme.primaryColor, flex: 1 }]}
280
+ onPress={dismiss}
281
+ activeOpacity={0.82}
282
+ >
283
+ <Text style={styles.primaryBtnText}>Выйти</Text>
284
+ </TouchableOpacity>
285
+ </View>
286
+ </View>
287
+ </View>
288
+ )}
289
+ </Animated.View>
290
+ </KeyboardAvoidingView>
291
+ </Animated.View>
292
+ )
293
+ }
294
+
295
+ function StarIcon({ size, filled, color }: { size: number; filled: boolean; color: string }) {
296
+ return (
297
+ <View style={{ width: size, height: size }}>
298
+ <Text
299
+ style={{
300
+ fontSize: size * 0.88,
301
+ lineHeight: size,
302
+ textAlign: 'center',
303
+ color: filled ? color : '#d0d0dc',
304
+ }}
305
+ >
306
+
307
+ </Text>
308
+ </View>
309
+ )
310
+ }
311
+
312
+ const styles = StyleSheet.create({
313
+ backdrop: {
314
+ ...StyleSheet.absoluteFillObject,
315
+ backgroundColor: 'rgba(0,0,0,0.48)',
316
+ justifyContent: 'center',
317
+ alignItems: 'center',
318
+ zIndex: 100,
319
+ paddingHorizontal: 20,
320
+ },
321
+ kav: {
322
+ width: '100%',
323
+ maxWidth: 480,
324
+ alignItems: 'center',
325
+ },
326
+ card: {
327
+ backgroundColor: '#fff',
328
+ borderRadius: 20,
329
+ borderTopWidth: 5,
330
+ paddingHorizontal: 24,
331
+ paddingBottom: 28,
332
+ paddingTop: 24,
333
+ width: '100%',
334
+ shadowColor: '#000',
335
+ shadowOffset: { width: 0, height: 10 },
336
+ shadowOpacity: 0.18,
337
+ shadowRadius: 24,
338
+ elevation: 20,
339
+ },
340
+ closeBtn: {
341
+ position: 'absolute',
342
+ top: 14,
343
+ right: 18,
344
+ zIndex: 10,
345
+ padding: 6,
346
+ },
347
+ closeBtnText: {
348
+ fontSize: 18,
349
+ fontWeight: '500',
350
+ },
351
+ title: {
352
+ fontSize: 20,
353
+ fontWeight: '700',
354
+ textAlign: 'center',
355
+ marginBottom: 6,
356
+ marginTop: 4,
357
+ paddingRight: 28,
358
+ lineHeight: 26,
359
+ },
360
+ description: {
361
+ fontSize: 14,
362
+ textAlign: 'center',
363
+ marginBottom: 16,
364
+ lineHeight: 20,
365
+ },
366
+ starsRow: {
367
+ flexDirection: 'row',
368
+ justifyContent: 'center',
369
+ flexWrap: 'wrap',
370
+ marginTop: 8,
371
+ marginBottom: 4,
372
+ },
373
+ skipBtn: {
374
+ alignSelf: 'center',
375
+ marginTop: 16,
376
+ paddingVertical: 6,
377
+ paddingHorizontal: 12,
378
+ },
379
+ skipText: {
380
+ fontSize: 14,
381
+ textDecorationLine: 'underline',
382
+ },
383
+ commentSection: {
384
+ marginTop: 16,
385
+ gap: 10,
386
+ },
387
+ resultSection: {
388
+ marginTop: 16,
389
+ alignItems: 'center',
390
+ gap: 12,
391
+ },
392
+ resultMsg: {
393
+ fontSize: 14,
394
+ textAlign: 'center',
395
+ lineHeight: 20,
396
+ fontWeight: '500',
397
+ },
398
+ commentInput: {
399
+ borderWidth: 1.5,
400
+ borderRadius: 12,
401
+ padding: 12,
402
+ fontSize: 15,
403
+ minHeight: 88,
404
+ lineHeight: 21,
405
+ },
406
+ primaryBtn: {
407
+ borderRadius: 12,
408
+ paddingVertical: 13,
409
+ paddingHorizontal: 24,
410
+ alignItems: 'center',
411
+ shadowOffset: { width: 0, height: 3 },
412
+ shadowOpacity: 0.22,
413
+ shadowRadius: 6,
414
+ elevation: 4,
415
+ },
416
+ primaryBtnText: {
417
+ color: '#fff',
418
+ fontSize: 15,
419
+ fontWeight: '700',
420
+ letterSpacing: 0.2,
421
+ },
422
+ btnDisabled: {
423
+ opacity: 0.48,
424
+ shadowOpacity: 0,
425
+ elevation: 0,
426
+ },
427
+ confirmOverlay: {
428
+ ...StyleSheet.absoluteFillObject,
429
+ backgroundColor: 'rgba(0,0,0,0.42)',
430
+ borderRadius: 20,
431
+ justifyContent: 'center',
432
+ alignItems: 'center',
433
+ zIndex: 20,
434
+ padding: 24,
435
+ },
436
+ confirmBox: {
437
+ backgroundColor: '#fff',
438
+ borderRadius: 16,
439
+ padding: 20,
440
+ width: '100%',
441
+ maxWidth: 360,
442
+ shadowColor: '#000',
443
+ shadowOffset: { width: 0, height: 4 },
444
+ shadowOpacity: 0.2,
445
+ shadowRadius: 12,
446
+ elevation: 10,
447
+ },
448
+ confirmText: {
449
+ fontSize: 14,
450
+ lineHeight: 20,
451
+ textAlign: 'center',
452
+ marginBottom: 16,
453
+ },
454
+ confirmBtns: {
455
+ flexDirection: 'row',
456
+ gap: 10,
457
+ },
458
+ confirmSecondary: {
459
+ flex: 1,
460
+ borderWidth: 1.5,
461
+ borderRadius: 12,
462
+ paddingVertical: 12,
463
+ alignItems: 'center',
464
+ },
465
+ confirmSecondaryText: {
466
+ fontSize: 14,
467
+ fontWeight: '600',
468
+ },
469
+ })
@@ -0,0 +1,27 @@
1
+ import {
2
+ attachmentDisplayName,
3
+ buildAttachmentDownloadRequest,
4
+ sanitizeFilename,
5
+ type AttachmentDownloadHandler,
6
+ } from '../attachmentUtils'
7
+ import Downloader from '../native/NativeChatSdkDownloader'
8
+ import type { GalleryAttachment } from '../types'
9
+
10
+ export const defaultAttachmentDownloader: AttachmentDownloadHandler = async (
11
+ attachment: GalleryAttachment,
12
+ ) => {
13
+ if (!Downloader) {
14
+ throw new Error(
15
+ 'ChatSDK: нативный модуль ChatSdkDownloader не подключён. ' +
16
+ 'Пересоберите приложение (Android: ./gradlew clean; iOS: pod install).',
17
+ )
18
+ }
19
+
20
+ const { downloadUrl, filename, headers, mime } = buildAttachmentDownloadRequest(attachment)
21
+ await Downloader.download({
22
+ url: downloadUrl,
23
+ filename: sanitizeFilename(filename) || attachmentDisplayName(attachment),
24
+ mime: mime || 'application/octet-stream',
25
+ headers,
26
+ })
27
+ }
@@ -0,0 +1,22 @@
1
+ import FilePicker from './native/NativeChatSdkFilePicker'
2
+ import type { AttachmentInput } from './types'
3
+
4
+ /** Открывает системный пикер файлов. Возвращает null если пользователь отменил выбор. */
5
+ export async function pickFiles(): Promise<AttachmentInput[] | null> {
6
+ if (!FilePicker) {
7
+ throw new Error(
8
+ 'ChatSDK: нативный модуль ChatSdkFilePicker не подключён. ' +
9
+ 'Пересоберите приложение (Android: ./gradlew clean; iOS: pod install).',
10
+ )
11
+ }
12
+
13
+ const picked = await FilePicker.pick({ multiple: true })
14
+ if (!picked || picked.length === 0) return null
15
+
16
+ return picked.map((f) => ({
17
+ uri: f.uri,
18
+ name: f.name,
19
+ type: f.mime || 'application/octet-stream',
20
+ size: f.size > 0 ? f.size : undefined,
21
+ }))
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ export { ChatSDK } from './ChatSDK'
2
+
3
+ export { ChatScreen } from './components/ChatScreen'
4
+ export { useChat } from './useChat'
5
+ export type { AttachmentDownloadHandler } from './attachmentUtils'
6
+ export type { ChatTheme } from './theme'
7
+ export type {
8
+ ChatSDKConfig,
9
+ ChatSDKUser,
10
+ ChatSDKDevice,
11
+ ChatSDKTheme,
12
+ MobileConfig,
13
+ ChatMessage,
14
+ ChatAttachment,
15
+ ChatOperator,
16
+ AttachmentInput,
17
+ GalleryAttachment,
18
+ ChatButton,
19
+ MessagesResponse,
20
+ NotificationPayload,
21
+ OperatorChangedEventPayload,
22
+ NewMessageEventPayload,
23
+ MessagesUpdatedEventPayload,
24
+ ChatSDKEventName,
25
+ SessionResponse,
26
+ SurveyConfig,
27
+ SurveyConfigResponse,
28
+ SDKState,
29
+ ChatStrings,
30
+ } from './types'
@@ -0,0 +1,49 @@
1
+ import { NativeEventEmitter, NativeModules, TurboModuleRegistry } from 'react-native'
2
+ import type { EventSubscription, TurboModule } from 'react-native'
3
+
4
+ export interface DownloadRequest {
5
+ url: string
6
+ filename: string
7
+ mime: string
8
+ headers: Record<string, string>
9
+ }
10
+
11
+ export interface DownloadProgress {
12
+ id: string
13
+ bytesWritten: number
14
+ totalBytes: number
15
+ }
16
+
17
+ export interface Spec extends TurboModule {
18
+ download(request: DownloadRequest): Promise<{ id: string; uri: string }>
19
+ addListener(eventName: string): void
20
+ removeListeners(count: number): void
21
+ }
22
+
23
+ const MODULE_NAME = 'ChatSdkDownloader'
24
+
25
+ function loadModule(): Spec | null {
26
+ try {
27
+ return TurboModuleRegistry.get<Spec>(MODULE_NAME)
28
+ } catch {
29
+ return (NativeModules[MODULE_NAME] as Spec | undefined) ?? null
30
+ }
31
+ }
32
+
33
+ const moduleRef = loadModule()
34
+
35
+ export default moduleRef
36
+
37
+ let emitter: NativeEventEmitter | null = null
38
+
39
+ function getEmitter(): NativeEventEmitter | null {
40
+ if (!moduleRef) return null
41
+ if (!emitter) emitter = new NativeEventEmitter(moduleRef as unknown as never)
42
+ return emitter
43
+ }
44
+
45
+ export function onDownloadProgress(
46
+ handler: (event: DownloadProgress) => void,
47
+ ): EventSubscription | null {
48
+ return getEmitter()?.addListener('ChatSdkDownloadProgress', handler) ?? null
49
+ }
@@ -0,0 +1,30 @@
1
+ import { NativeModules, TurboModuleRegistry } from 'react-native'
2
+ import type { TurboModule } from 'react-native'
3
+
4
+ export interface PickedFile {
5
+ uri: string
6
+ name: string
7
+ mime: string
8
+ size: number
9
+ }
10
+
11
+ export interface PickOptions {
12
+ multiple: boolean
13
+ mimeFilter?: string[]
14
+ }
15
+
16
+ export interface Spec extends TurboModule {
17
+ pick(options: PickOptions): Promise<PickedFile[] | null>
18
+ }
19
+
20
+ const MODULE_NAME = 'ChatSdkFilePicker'
21
+
22
+ function loadModule(): Spec | null {
23
+ try {
24
+ return TurboModuleRegistry.get<Spec>(MODULE_NAME)
25
+ } catch {
26
+ return (NativeModules[MODULE_NAME] as Spec | undefined) ?? null
27
+ }
28
+ }
29
+
30
+ export default loadModule()