Skip to content

About

RevBack Chat SDK for Android — support chat and push notifications inside your app

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

RevBack Chat SDK для Android

Documentation in English

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)

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 — на вашей стороне.

Документация и 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 (канал, операторы, история переписки) остаётся по условиям вашего тарифа.

About

RevBack Chat SDK for Android — support chat and push notifications inside your app

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages