RevBack Chat SDK встраивает чат поддержки RevBack в мобильное приложение: переписка с оператором и AI-ассистентом, история, вложения, счётчик непрочитанного и push-уведомления. Два артефакта — ядро без единого экрана и готовый экран чата на Jetpack Compose поверх него: берите готовый экран, если не хотите рисовать чат сами, или только ядро, если интерфейс у вас свой.
Ядро написано на Kotlin Multiplatform. Канал распространения Android-части — Maven Central, парной версии для iOS — Swift Package Manager, репозиторий revback-chat-ios.
- Переписка визитёра с оператором и AI-ассистентом, системные сообщения.
- История переписки с подгрузкой вверх; кэш беседы ускоряет холодный старт.
- Вложения в обе стороны: загрузка и отправка своих файлов, скачивание файлов оператора и своих; у видео — обложка, длительность и размеры.
- Статусы своих сообщений: отправляется, отправлено (сервер принял), прочитано оператором, не отправлено (с повтором).
- Очередь отправки на диске: неотправленное переживает перезапуск процесса (пока история хранится на диске — так по умолчанию).
- Счётчик непрочитанного вне экрана чата — для бейджа на кнопке «Поддержка».
- Push-уведомления через FCM: показ, свой канал уведомлений, переход к беседе по тапу.
- Связывание визитёра с пользователем приложения (
identify) по токену, который выпускает ваш бэкенд. - Атрибуты пользователя, которые видит оператор (
setAttributes). - Готовый экран чата на Jetpack Compose — отдельным артефактом; ядро работает и без него.
- Телеметрия SDK для диагностики интеграции (без идентификаторов визитёра и токенов).
| Что | Значение |
|---|---|
| Минимальная версия Android | 7.0 (API 24) |
compileSdk проекта |
35 и выше — так требуют метаданные AAR зависимостей SDK (модули собраны под compileSdk 36) |
| Байткод модулей | Java 17 (sourceCompatibility/targetCompatibility = 17) |
| Kotlin у хоста | 2.2 и выше: в публичном API ядра есть kotlin.time.Instant |
| Jetpack Compose у хоста | нужен только для встраиваемого RevbackChatScreen; RevbackChatUI.open() обходится без Compose |
| Firebase Cloud Messaging | подключает само приложение — модуль FCM SDK за собой не тянет |
| Сборка этого репозитория | JDK 21 (байткод всё равно 17) |
Разрешение INTERNET объявляют сами модули. Разрешение POST_NOTIFICATIONS (Android 13+)
объявляет и запрашивает приложение — SDK системных запросов не делает; когда его спрашивать —
см. «Разрешение на уведомления».
SDK не требует переделывать сетевой слой приложения: на Android сеть SDK идёт через OkHttp напрямую, Ktor не используется и в зависимости не попадает. Всё ниже Gradle подтягивает сам по POM артефактов; указанная версия — минимум: если в приложении версия выше, Gradle возьмёт её.
| Зависимость | Версия в POM | Что это значит для приложения |
|---|---|---|
OkHttp (com.squareup.okhttp3:okhttp) |
4.12.0 | приложение на OkHttp 4.12+ или 5.x поднимет его до своей версии — SDK работает и с OkHttp 5.x. Только runtime-скоуп: в compile-classpath приложения не попадает. Объявлен только артефакт okhttp (не okhttp-jvm/okhttp-android), дублей классов у приложений на OkHttp 5 нет |
Ktor (io.ktor:*) |
— | не требуется и не тянется: Ktor приложения (2.x или 3.x) SDK не затрагивает |
kotlinx-coroutines-core / kotlinx-coroutines-android |
1.10.2 | минимум 1.10.2; kotlinx-coroutines-core виден при компиляции приложения (в публичном API — StateFlow/SharedFlow) |
| Kotlin stdlib | 2.2.21 | Kotlin у приложения — 2.2 и выше (см. «Требования») |
kotlinx-serialization-json |
1.9.0 | — |
SQLDelight (app.cash.sqldelight:runtime, android-driver, coroutines-extensions; в дереве зависимостей Gradle runtime и coroutines-extensions разрешаются в runtime-jvm и coroutines-extensions-jvm, в POM они так и записаны) |
2.2.1 | на нём кэш беседы. runtime виден при компиляции приложения (compile-скоуп), драйвер и coroutines-extensions — только runtime. Если у приложения свой SQLDelight 2.x, Gradle возьмёт для каждого артефакта SQLDelight старшую из запрошенных версий: приложение на версии ниже 2.2.1 получит 2.2.1, на версии выше — SDK получит версию приложения. На других версиях SQLDelight SDK не проверялся |
androidx.core:core-ktx (ядро и экран) |
1.16.0 | compileSdk приложения 35 и выше, Android Gradle Plugin 8.6.0 и выше (метаданные AAR) |
Jetpack Compose (экран): androidx.compose.foundation:foundation, androidx.compose.ui:ui |
1.9.0 | без Compose BOM и без Material: версии Compose приложения SDK не меняет — приложение на более свежей Compose (в т. ч. Compose Multiplatform 1.9.1, на Android это Compose 1.9.3) поднимет экран до своей версии. compileSdk 35 и выше, AGP 8.6.0 и выше |
androidx.activity:activity-compose, androidx.lifecycle:lifecycle-runtime-compose (экран) |
1.10.1 / 2.9.4 | минимум; более свежие версии приложения экран принимает |
| Минимальная версия Android | API 24 | Android 7.0 и выше |
Полный список сторонних компонентов с лицензиями — в THIRD-PARTY-NOTICES.
В iOS-версии SDK сеть (Ktor на NSURLSession) собрана внутрь статического XCFramework и с Ktor приложения не пересекается.
Артефакты раздаются через Maven Central, отдельный адрес репозитория не нужен:
dependencies {
// готовый экран чата; ядро приезжает транзитивно
implementation("ai.revback:revback-chat-ui:0.1.0")
// или только ядро — если экран рисуете сами
implementation("ai.revback:revback-chat-core:0.1.0")
}Если ваш код читает Message.sentAt или зовёт conversation.markRead() (с аргументом и без) — в них
kotlin.time.Instant, а в Kotlin 2.2.x он под opt-in:
kotlin {
compilerOptions.optIn.add("kotlin.time.ExperimentalTime")
}Ключ канала (widgetKey) берётся в кабинете RevBack, канал «Чат в приложении». Ключ публичен
по дизайну — он извлекается из APK и секретом не является; секрет канала, которым ваш бэкенд
подписывает токен личности пользователя, в приложение не кладётся и живёт на вашем бэкенде.
Инициализация — в Application.onCreate, до любых обращений к SDK:
import ai.revback.chat.RevbackChat
import ai.revback.chat.RevbackChatConfig
import ai.revback.chat.init
import android.app.Application
class App : Application() {
override fun onCreate() {
super.onCreate()
RevbackChat.init(this, RevbackChatConfig(widgetKey = "wk_…"))
}
}Любой вызов до init безопасен: no-op с записью в лог, а setPushToken придержит токен и отправит
его после init. Так же не теряются conversation.open(), close() и markRead(): SDK запоминает
их и применяет к беседе сразу после init.
SDK работает только в главном процессе приложения. Application.onCreate исполняется в каждом процессе
(например, AppMetrica держит свой процесс :AppMetrica), и init в неглавном процессе — no-op с одной
строкой предупреждения в логе: ядро там не запускается, а остальные вызовы ведут себя как «до init».
Проверять процесс перед init не нужно. Главный — процесс приложения по умолчанию
(applicationInfo.processName); если имя процесса определить не удалось, процесс считается главным.
По умолчанию SDK хранит ленту и очередь отправки в кэше на диске: на холодном старте лента видна сразу, а неотправленное переживает перезапуск процесса. Если переписка не должна оставаться на устройстве (банк, медицина), выключите хранение:
import ai.revback.chat.RevbackChat
import ai.revback.chat.RevbackChatConfig
import ai.revback.chat.init
import android.app.Application
class App : Application() {
override fun onCreate() {
super.onCreate()
RevbackChat.init(this, RevbackChatConfig(widgetKey = "wk_…").withPersistHistory(false))
}
}- Лента и очередь отправки живут только в памяти процесса: история приходит с сервера при каждом старте, а неотправленное при перезапуске процесса пропадает.
- На диске остаются идентификатор визитёра и подтверждённая личность, счётчик непрочитанного, атрибуты пользователя и оформление канала — без них не продолжить ту же беседу.
- Готовый экран не сохраняет копии выбранных файлов: своё вложение он показывает, скачивая его с сервера (см. «Известные ограничения»). Файл, который пользователь открыл в системном просмотрщике, лежит в кэше приложения до закрытия экрана.
- Свой экран сам решает, где держать файлы, которые передаёт в
FileSource.withLocalUri: SDK их не удаляет.
При выходе пользователя (reset()) и при смене пользователя SDK удаляет каталог revback-chat в кэше
приложения со всеми файлами, которые там сохранял готовый экран, — при любой настройке persistHistory.
С выключенным хранением этот каталог очищается ещё и при каждом запуске SDK.
Текущую настройку можно прочитать: RevbackChat.config?.persistHistory (config — null до init).
import ai.revback.chat.ui.RevbackChatUI
import ai.revback.chat.ui.RevbackChatScreen
// открыть поверх приложения — например, по тапу на «Поддержка»
RevbackChatUI.open(context)
// или встроить в свою навигацию (Compose)
RevbackChatScreen(onClose = { navController.popBackStack() })- Экран повторяет вид виджета RevBack на сайте: шапка в цвете канала, входящие с подписью отправителя, свои сообщения со статусами, вложения, композер.
- Тема у экрана своя (светлая): Material-тема хоста не наследуется.
onClose = null— крестика в шапке нет, экран закрывает хост.- Экран сам открывает и закрывает беседу по жизненному циклу — звать
conversation.open()/close()рядом с ним не нужно. - Вложения показываются системным просмотрщиком через собственный
FileProvider(${applicationId}.revback.chat.files) — отдельной настройки в хосте не требуется. RevbackChatUI.open()доRevbackChat.initоткрывает экран: он показывает «Чат временно недоступен» и оживает сам, как только вы позовётеinit. Еслиinitне случится никогда, экран так и останется на этом состоянии.
Всё, что нужно своему экрану, лежит в RevbackChat.conversation. Модели и результаты — из пакета
ai.revback.chat:
import ai.revback.chat.FileSource
import ai.revback.chat.RevbackChat
import ai.revback.chat.UploadResult
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
val conv = RevbackChat.conversation
conv.open() // экран показан: живёт chat-сокет, пуши о новых сообщениях не показываются баннером
conv.close() // экран скрыт
lifecycleScope.launch { conv.messages.collect { render(it) } } // StateFlow<List<Message>>
lifecycleScope.launch { conv.state.collect { renderState(it) } } // Empty | Loading | Ready | Failed
// отправка, повтор, догрузка и прочтение — suspend-функции
lifecycleScope.launch { conv.send("Здравствуйте!") } // оптимистично: сразу в ленте, статус Sending
lifecycleScope.launch { conv.retry(messageId) } // повтор своего Failed
lifecycleScope.launch { conv.loadMore() } // страница истории вверх
lifecycleScope.launch { conv.markRead() } // прочитано всё до текущего момента
// есть ли страница выше — это тоже поток, а не значение: conv.hasMore: StateFlow<Boolean>
// (читайте подпиской или conv.hasMore.value)Причина неудачной отправки — в MessageStatus.Failed.reason и в событии телеметрии
TelemetryEvent.SendFailed. Причина rejected значит, что сервер принял запрос, но сообщение не сохранил —
например, канал выключен или после проверки вложений от сообщения ничего не осталось. Если сообщение на
сервере всё же есть, оно само перейдёт в Sent, когда придёт с сервера.
Вложение сначала загружается, потом отправляется — send() принимает только результат upload():
lifecycleScope.launch {
val source = FileSource(fileName = "photo.jpg", mime = "image/jpeg", bytes = fileBytes)
when (val result = conv.upload(source).await()) {
is UploadResult.Done -> conv.send("", listOf(result.attachment))
is UploadResult.Failed -> showError(result.reason)
else -> Unit // Cancelled и будущие варианты
}
// байты вложения — оператора и своего; null — вложение недоступно
val bytes = conv.attachmentBytes(message.id, index = 0)
}Своё вложение показывайте из своего файла (Attachment.localUri — то, что вы передали в
FileSource.withLocalUri), а если его уже нет — через attachmentBytes, как вложение оператора: сервис
отдаёт и свои файлы, например после переустановки или на другом устройстве. Исключение — своё сообщение,
которое сервер ещё не подтвердил (Sending, Failed): для него attachmentBytes сразу вернёт null, без
запроса.
Видео можно отправить с обложкой: кадр, длительность и размеры снимает ваш экран при выборе файла,
обложка уходит отдельной загрузкой, а её сбой отправку не валит — видео уйдёт без обложки. У фото
достаточно размеров. Превью собирается цепочкой: MediaPreview() и withThumbnail, withDurationMs,
withSize — обложка задаётся вместе с типом, ширина вместе с высотой. Значения вне допустимых границ
(длительность, размеры) ядро отбрасывает до сети.
import ai.revback.chat.Conversation
import ai.revback.chat.FileSource
import ai.revback.chat.MediaPreview
import ai.revback.chat.Message
import ai.revback.chat.UploadResult
// видео с обложкой: кадр, длительность и размеры снимает ваш экран при выборе файла
suspend fun sendVideo(
conv: Conversation,
videoBytes: ByteArray,
coverJpeg: ByteArray?, // кадр из первой секунды; JPEG или WebP, до 640 px, до 200 КБ
durationMs: Long,
width: Int, // в ориентации показа, всегда парой с height
height: Int,
): UploadResult {
var preview = MediaPreview()
.withDurationMs(durationMs)
.withSize(width, height)
if (coverJpeg != null) preview = preview.withThumbnail(coverJpeg, "image/jpeg")
val source = FileSource(fileName = "clip.mp4", mime = "video/mp4", bytes = videoBytes)
.withPreview(preview)
val result = conv.upload(source).await()
if (result is UploadResult.Done) conv.send("", listOf(result.attachment))
return result // Failed, Cancelled и будущие варианты — покажите ошибку по-своему
}
// обложка видео в ленте; null — обложки нет: покажите ссылку на файл
suspend fun videoCover(conv: Conversation, message: Message, index: Int): ByteArray? {
val attachment = message.attachments[index]
// attachment.width, attachment.height, attachment.durationMs — null, если неизвестны
if (attachment.thumbnailBlobRef == null) return null
return conv.attachmentThumbnail(message.id, index)
}Своему видео, которое сервер ещё не подтвердил, обложку с сервера не получить — показывайте кадр, снятый при выборе файла.
Счётчик непрочитанного вне экрана чата:
// новый слушатель сразу получает текущее число (если оно уже известно), дальше — каждое изменение;
// колбэк приходит не с главного потока
RevbackChat.setUnreadListener { unread ->
badge.post { badge.text = if (unread.count > 99) "99+" else unread.count.toString() }
}
// или потоком
lifecycleScope.launch {
RevbackChat.unreadCount.collect { unread ->
badge.text = if (unread.count > 99) "99+" else unread.count.toString()
}
}Целиком собираемый пример — в sample/android-custom.
FCM SDK подключает само приложение; SDK RevBack получает токен и разбирает полезную нагрузку:
import ai.revback.chat.PushAction
import ai.revback.chat.RevbackChat
import ai.revback.chat.onMessageReceived
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class AppMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) = RevbackChat.setPushToken(token)
override fun onMessageReceived(message: RemoteMessage) {
when (RevbackChat.onMessageReceived(this, message.data)) {
is PushAction.NotOurs -> handleOwnPush(message) // ваши собственные пуши
else -> Unit // наши показаны внутри
}
}
}
// и при каждом старте процесса
FirebaseMessaging.getInstance().token.addOnSuccessListener { RevbackChat.setPushToken(it) }Тап по пушу приводит в launcher-активити приложения; беседу оттуда достаёт SDK:
import ai.revback.chat.RevbackChat
import ai.revback.chat.conversationId
RevbackChat.conversationId(intent)?.let { conversationId -> openSupportChat(conversationId) }Если launcher-активити не пробрасывает extras (например, сплэш) — задайте свой интент тапа:
import ai.revback.chat.RevbackChatNotifications
import android.app.PendingIntent
import android.content.Intent
RevbackChatNotifications.contentIntentFactory = { ctx, conversationId ->
PendingIntent.getActivity(
ctx, 0,
Intent(ctx, ChatActivity::class.java).putExtra("conv", conversationId),
PendingIntent.FLAG_IMMUTABLE,
)
}- Канал уведомлений
revback_supportсоздаёт сам модуль. - Иконка уведомления:
RevbackChatNotifications.smallIconOverride = R.drawable.ic_notification. - Строки уведомлений — английские по умолчанию, русские в
values-ru; другой язык добавляется теми же именами ресурсов в вашемvalues-xx(ресурсы хоста побеждают ресурсы AAR). - Чтобы пуши доходили, в кабинете RevBack на канале «Чат в приложении» должен быть загружен
service-account вашего проекта Firebase с правом
cloudmessaging.messages.create. - Auto Backup: идентификатор визитёра лежит в
shared_prefs/revback_chat_visitor.xml. Если у приложения своиbackup-rules— включите этот файл, иначе переустановка даст нового визитёра и потерянную переписку.
Проверить доставку на конкретном устройстве помогает RevbackChat.sendTestPush() — это
suspend-функция, её зовут из корутины; диагностику интеграции отдают RevbackChat.debugInfo
(это DebugInfo?: до init там null) и RevbackChat.setTokenSyncListener { … }.
На Android 13+ уведомления показываются только с разрешением POST_NOTIFICATIONS, и спрашивает его
приложение. Ответ поддержки приходит не сразу, и пуш — единственный способ о нём узнать, поэтому удачный
момент для запроса — сразу после того, как пользователь отправил первое сообщение в чат:
- сначала отправьте сообщение, не задерживая его, потом покажите поверх чата короткий экран-объяснение (например, «Сообщим, когда ответит поддержка») с одной кнопкой, которая открывает системный запрос;
- показывайте его, только если уведомления чата не доходят: нет разрешения, уведомления приложения
выключены (
NotificationManagerCompat.areNotificationsEnabled()) или пользователь выключил каналRevbackChatNotifications.CHANNEL_ID. Если пользователь уже отказал и система запрос больше не показывает, а также ниже Android 13, кнопка должна вести в настройки уведомлений приложения; - отказ не переспрашивайте при каждом сообщении.
Первое отправленное сообщение видно в потоке conversation.messages: своё новое сообщение появляется в
ленте сразу со статусом MessageStatus.Sending. Повтор неудачного сообщения сохраняет его id — новым
его не считайте.
Готовый экран на ON_STOP закрывает беседу: перестаёт работать chat-сокет, и уведомления о новых
сообщениях снова показываются баннером. Поэтому экран-объяснение не должен его останавливать:
- чат открыт через
RevbackChatUI.open()— этоRevbackChatActivity. Покажите объяснение отдельной активити с прозрачной темой (android:windowIsTranslucent) и запускайте её из контекста самойRevbackChatActivity(найти её помогаетApplication.ActivityLifecycleCallbacks): так она ляжет в ту же задачу прямо над чатом, а чат под ней останется запущенным. Непрозрачная активити поверх чата его остановит; - чат встроен через
RevbackChatScreen— покажите объяснение диалогом или шторкой в той же активити.
identify связывает визитёра с пользователем вашего приложения. Токен выпускает ваш бэкенд:
JWT HS256, ключ — секрет канала из кабинета как есть, срок — на ваше усмотрение (советуем 1–5 минут).
В токене — id пользователя (sub), остальное по желанию. Один токен — один вход: на каждый запрос
SDK выпускайте новый токен со случайным jti, не кэшируйте его (повтор токена из другой сессии —
410). Необязательные claims названы как в OpenID Connect (формат токена identify v2): name,
email, phone_number — E.164 (+79001234567; пробелы, дефисы и скобки сервер уберёт сам; прежний
phone сервер игнорирует — переименуйте его); email_verified, phone_number_verified — подтвердил
ли ваш бэкенд почту и телефон; amr, idp, auth_time — как и когда пользователь вошёл; ids —
ваши идентификаторы пользователя объектом «ключ → строка» (например {"crm_id": "…"}; их
получают инструменты ассистента, модели они не показываются); attributes — подписанные атрибуты.
Поле неверного вида вход не роняет: сервер его не сохраняет, identify проходит без ошибки — отказ
(422) бывает только по безопасности токена. SDK токен не разбирает, поэтому переход на v2 —
правка только вашего бэкенда. Точные правила, готовые сниппеты для Node, PHP, Python и Go и
тест-вектор — в описании операции identify в OpenAPI RevBack
(https://api.revback.ai/v1/widget/openapi.json). Приложение просит токен у своего бэкенда и отдаёт
его SDK как есть:
RevbackChat.identify { done ->
yourBackend.revbackToken { token: String? -> done(token) }
}Запрос токена к своему бэкенду делайте отдельным HTTP-клиентом — без общего обработчика ответа 401
(например, Authenticator OkHttp или обновления токена в плагине Auth Ktor), который обновляет сессию,
повторяет запрос или выводит пользователя из аккаунта:
- отказ этой ручки — просто «токена нет»: отдайте SDK
done(null), чат останется анонимным. Через общий обработчик ошибочный401вспомогательной ручки выкинул бы пользователя из аккаунта, а авто-обновление могло бы повторить запрос уже под другой (гостевой) сессией; - повторы не нужны: истёкший токен SDK попросит сам, а ответа ждёт не дольше 10 секунд;
- пользователь не вошёл — ручку не зовите, сразу
done(null); - пользователь вышел или сменился, пока запрос летел, — пришедший токен не отдавайте, верните
done(null).
Подтверждение личности включается на канале отдельно (RevbackChat.branding.value?.features.identify),
и токен проверяет сервис, а не SDK. Если возможность на канале выключена или токен не принят,
визитёр остаётся анонимным, а причина приходит событием TelemetryEvent.IdentifyFailed(reason);
если сервис отклонил токен из-за его содержимого, причина называет поле, например aud/mismatch.
Поставьте RevbackChat.setTelemetryListener { … } до первого identify и прогоните сценарий
со своим бэком, прежде чем полагаться на него в проде.
Звать identify нужно при каждом старте процесса, иначе визитёр останется анонимным. Ответа
провайдера SDK ждёт не дольше 10 секунд, побеждает первый вызов done; done(null) — отказ хоста,
личность остаётся прежней. Если срок токена успел истечь, SDK сам один раз попросит у провайдера новый.
Если у подтверждённого пользователя пропала сессия (например, истекла), анонимную сессию под его визитёром
сервер больше не выдаёт. Тогда SDK заводит нового анонимного визитёра — как при выходе: лента и очередь
отправки очищаются, неотправленное падает в Failed с причиной identity_lost, в ленте приходит
ChatEvent.Reset, — и присылает событие телеметрии TelemetryEvent.IdentityLost. Если пользователь в
приложении всё ещё вошёл, позовите identify ещё раз. Слушатель телеметрии у SDK один — держите в нём и
логирование:
import ai.revback.chat.IdentityTokenProvider
import ai.revback.chat.RevbackChat
import ai.revback.chat.TelemetryEvent
import android.util.Log
fun installSupportTelemetry(isSignedIn: () -> Boolean, tokenProvider: IdentityTokenProvider) {
// колбэк приходит не с главного потока
RevbackChat.setTelemetryListener { event ->
Log.i("RevBack", event.toString())
if (event is TelemetryEvent.IdentityLost && isSignedIn()) {
RevbackChat.identify(tokenProvider)
}
}
}При выходе пользователя из аккаунта зовите RevbackChat.reset() (suspend): это новый визитёр,
снятая подписка на пуши и недоступная прежняя переписка. Если экран чата в этот момент открыт,
закрывает или перерисовывает его хост — по событию ChatEvent.Reset в conversation.events.
Выход доводится до конца, даже если корутина, из которой его позвали, отменена.
Порядок при смене пользователя: reset() (suspend), и только после возврата — identify() и
setAttributes() нового пользователя, из той же корутины. Если вызвать их, не дождавшись выхода
(scope.launch { RevbackChat.reset() } и сразу identify { … }), SDK отнесёт их к прежнему
пользователю: выход их отбросит, новый визитёр останется анонимным и без атрибутов, а в телеметрии придут
TelemetryEvent.IdentifyFailed("logging_out") / TelemetryEvent.AttributesFailed("logging_out").
Вызовы, пришедшие, пока reset() уже исполняется, ждут его конца и достаются новому визитёру.
scope.launch {
RevbackChat.reset() // дождались выхода
RevbackChat.identify(tokenProvider) // новый пользователь
RevbackChat.setAttributes(mapOf("name" to name))
}sample/android— хост с кнопкой «Поддержка», открывающей готовый экран.sample/android-custom— свой экран чата поверх одного лишь ядра: лента, отправка, фото, «Загрузить ещё», повтор неудачной отправки, бейдж непрочитанного.
Ключ канала передаётся свойством сборки, в репозитории его нет:
./gradlew :sample-android:installDebug -PREVBACK_SAMPLE_WIDGET_KEY=wk_…
./gradlew :sample-custom-android:installDebug -PREVBACK_SAMPLE_WIDGET_KEY=wk_…Firebase в демо-приложениях не подключён — пуши на них не проверяются. Чтобы посмотреть пуши,
добавьте в демо свой google-services.json и свой проект Firebase.
export JAVA_HOME=/path/to/jdk-21
./gradlew :revback-chat-ui:assembleRelease # AAR готового экрана
./gradlew :revback-chat-core:assembleRelease # AAR ядра
./gradlew :revback-chat-core:testDebugUnitTest :revback-chat-ui:testПубличный API обоих модулей зафиксирован слепками (core/api/, ui-android/api/):
./gradlew apiCheck роняет сборку, если сигнатуры разъехались со слепком.
Часть возможностей зависит от настроек вашего канала и версии сервиса: подтверждение личности,
вложения, индикатор «печатает», счётчик непрочитанного, атрибуты, правка и удаление сообщений.
Актуальный набор приезжает потоком RevbackChat.branding: StateFlow<Branding?>: читайте его
подпиской или как RevbackChat.branding.value?.features. О выключенной возможности SDK сообщает
событием телеметрии, а не молчаливым отказом.
Версии — по SemVer. Пока версия 0.x, ломающие изменения возможны в минорной версии (0.1 → 0.2) и
описываются в заметках к выпуску (CHANGELOG.md в этом репозитории и текст GitHub Release); патч-версии
(0.1.0 → 0.1.1) ничего не ломают. Правила ниже действуют начиная с 0.1.0.
Как устроен API, чтобы обновления не ломали вашу сборку:
- Модели SDK создаёт только SDK. У
Message,Attachment,Branding,Featuresи других моделей данных нет публичного конструктора иcopy— поэтому новые поля появляются без ломающих изменений. Новые поля добавляются только в конец. Не деструктурируйте модели SDK (val (id, isMine) = message) — обращайтесь к свойствам по имени. Из Java конструкторы моделей технически видны, но не поддерживаются: пользуйтесь фикстурами. - Тесты и превью вашего экрана. Модели для них собирают фикстуры из пакета
ai.revback.chat.testing:MessageFixture,AttachmentFixture,SenderFixture,FeaturesFixture,BrandingFixture,TypingStateFixture,UnreadCountFixture, а состояния и события с данными — функцииStateFixtures. В Kotlin они помечены@RevbackTestApiи требуют@OptIn(RevbackTestApi::class); в боевом коде их не используйте. Пакетai.revback.chat.testing— не боевой API: правила этого раздела на него не распространяются, и фикстуры могут меняться в обычных версиях, не только в ломающих. - Настройки и файлы. В конструкторе
RevbackChatConfig,FileSource— только обязательное, уMediaPreviewобязательного нет (MediaPreview()); остальное задаётся цепочкойwith…(RevbackChatConfig("wk_…").withPersistHistory(false),MediaPreview().withSize(1080, 1920)). Новая настройка — это новый методwith…, существующий код не меняется. - Состояния и события (
ConnectionState,ConversationState,MessageStatus,Sender,ChatEvent,UploadResult,PushAction,TelemetryEvent,TestPushResult,TestPushRejection) намеренно НЕsealed: набор вариантов растёт, и новый вариант не должен ломать вашу сборку. Поэтомуwhen-выражение потребует веткуelse, а в Swift (iOS-версия SDK) разбор идёт черезis/as?. Вариант проверяйте по типу (is ConnectionState.Connected), а не по равенству: вариант без данных может в будущем получить данные. Варианты с данными создаёт только SDK, в тестах — функцииStateFixtures. - Незнакомые значения сервера не теряются.
AttachmentKind— не enum: у незнакомого видаisKnown == false, аwireхранит строку сервера; известные виды — свойства companion (AttachmentKind.Photo). Сервер уже сегодня может прислать вид вложения вне восьми именованных (напримерunknown) — он придёт сisKnown == false. Отправитель незнакомого вида приходит какSender.Other(kind, name, avatarUrl). - Сообщение неожиданной формы не ломает ленту. Если содержимое сообщения пришло в форме, которой эта
версия SDK не знает, сообщение всё равно попадает в ленту — чужим живым сообщением
(
isMine == false,deleted == false) без текста и без вложений: покажите ему заглушку вроде «Не удалось отобразить сообщение». Своё сообщение в такой же форме — это отклонённая отправка, у неё свойMessageStatus.Failed, и заглушка ей не нужна. Вложение, которое не разобралось, приезжает какAttachmentвидаAttachmentKind.Otherбез имени, MIME-типа и ссылок — его место в списке и индексы соседних вложений при этом сохраняются; подпишите его «Данный тип файла не поддерживается». Встроенный экран SDK делает и то, и другое сам. - Экран, встроенный до настройки, оживает сам.
conversation.open(), вызванный ДОinit(например, экран чата лежит во вкладке, а ключ канала приложение получает по сети), не теряется: SDK запомнит его и откроет беседу сразу, как только модуль соберётся, — и ровно так же переоткроет её на новом модуле при смене ключа на живом процессе. До настройки беседа отвечаетConversationState.Failed("not_configured")— покажите на этом состоянии экран «чат недоступен» без поля ввода. Парныйclose()при уходе экрана обязателен: пока беседа открыта, SDK не показывает баннеры пушей о новых сообщениях. Встроенный экран SDK делает всё это сам. - Перечисление
UnreadSource: в совместимых версиях значения в него не добавляются. PushProvider— не enum, а открытый набор, какAttachmentKind: известные провайдеры — свойства companion (PushProvider.Fcm,Apns,Rustore,Hms), новые появляются новыми константами в совместимых версиях. Сравнивайте по равенству (==), а не исчерпывающимwhen. Провайдер, которого ваша версия SDK ещё не знает, задаётся строкой сервера:PushProvider.fromWire("…"). Пустая строка даётPushProvider.Fcm— не передавайте вfromWireнепроверенное значение из своего конфига.RevbackChatDelegate: новые методы в него не добавляются — в Swift все методы протокола обязательны. Новые события приходят черезonEventлибо отдельными слушателями.ConversationиUploadреализует только SDK. В них могут появляться новые методы; в тестах оборачивайте их своим интерфейсом, а не реализуйте напрямую.- Функции: у существующих публичных функций не появляется новых параметров, даже со значением по умолчанию — для Android это бинарно несовместимо, а в Swift значений по умолчанию нет вовсе. Новая возможность — новая функция или перегрузка.
- Своё вложение, ещё не подтверждённое сервером, без локальной копии не показывается. Своё вложение
экран рисует из копии файла в кэше приложения, а без неё (кэш почищен системой, приложение
переустановлено, сообщение отправлено с другого устройства,
persistHistory = false) — скачивает с сервера. Пока сервер не подтвердил отправку (статусыSendingиFailed), скачать нечего: у такого сообщения без копии видно только имя файла. Как только отправка подтверждена, картинка и обложка видео появляются сами. - Готовый экран отправляет картинку как есть, без перекодирования. Как картинку сервер показывает только PNG, JPEG, GIF, WebP, AVIF и BMP; фото в другом формате (например, HEIC) уходит файлом — в ленте у него имя файла вместо картинки. Своему экрану перекодировать такие картинки перед отправкой нужно самому.
- Наборы вариантов пополняются от версии к версии: публичные иерархии состояний и событий не
sealed, поэтомуwhen-выражение по ним требует веткуelse— полный перечень иерархий и правила см. в разделе «Совместимость и версии». - Тёмной темы у экрана нет. Палитра готового экрана Android зафиксирована светлой — как у виджета RevBack на сайте; настройки темы в SDK тоже нет.
RevbackChatActivityобъявлена сlaunchMode="singleTask": повторныйRevbackChatUI.open()не создаёт второй экран, но снимает со стека активности хоста, открытые поверх чата в той же задаче. ВстраиваемогоRevbackChatScreenэто не касается.- Колбэки SDK приходят из корутин ядра, не с главного потока: диспетчеризация на UI — на вашей стороне.
- Документация интегратора: https://revback.ai/docs/mobile-app
- Версия для iOS: revback-ai/revback-chat-ios
Заводите issue в этом репозитории. Помогает приложить:
- версию SDK (
RevbackChat.debugInfo?.sdkVersion) и версию Android с моделью устройства; - шаги воспроизведения и ожидаемое поведение;
- события
RevbackChat.setTelemetryListener { … }за время проблемы.
Не публикуйте в issue ключ канала, пуш-токен и идентификатор визитёра — они есть в
RevbackChat.debugInfo; если они нужны для разбора, пришлите их через кабинет RevBack.
Вопросы по каналу, тарифу и учётной записи решаются в кабинете https://revback.ai, а не здесь.
Репозиторий — Android-часть SDK: исходники ядра и готового экрана, демо-приложения и сборочные файлы. Разработка ведётся во внутреннем монорепозитории вместе с iOS-частью, а сюда содержимое переносится при выпуске версии.
Pull-запросы мы не вливаем: ветка здесь перезаписывается синхронизацией, и любой коммит поверх неё исчезнет со следующим выпуском. Если нашли ошибку или сделали патч — заводите issue и приложите diff (или ссылку на свою ветку): правку перенесём во внутренний репозиторий, а в issue напишем, в какой версии она приедет сюда.
Apache License 2.0 — см. LICENSE.
Сторонние компоненты, от которых зависят артефакты SDK, и их лицензии перечислены в THIRD-PARTY-NOTICES. Если вы публикуете приложение — перенесите их содержимое (вместе со своими уведомлениями) на экран лицензий приложения: наш код попадает к вашим пользователям внутри вашей сборки.
Лицензия распространяется на код SDK. Доступ к сервису RevBack (канал, операторы, история переписки) остаётся по условиям вашего тарифа.