Встраивание чата поддержки в Android-приложение
Для команд мобильных приложений. SDK Android — ru.shtab.helpdesk:helpdesk-android. Проверить встраивание до своей установки можно в песочнице мобильных SDK. iOS — отдельное руководство.
Beta 1.0.0-beta.2. Изменения и известные ограничения версии приходят с комплектом поставки. До 1.0 API может меняться, после — только патчи 1.0.x.
1. Как устроено
Тонкий натив и весь чат в WebView. SDK открывает свой экран (HelpdeskChatActivity) с WebView, который грузит UI чата с инсталляции — https://<публичный домен>/widget/mobile/. Функции чата приходят в приложение обновлением сервера, без нового SDK и ревью стора.
Если UI инсталляции недоступен — нет сети при открытии чата, сервер старше и не знает /widget/mobile/, UI не поддерживает версию моста SDK (bridge.json), страница не загрузилась или не ответила за 20 с, — SDK показывает вшитую копию UI той же сборки. Она открывается под тем же адресом инсталляции, поэтому сессия, очередь неотправленных сообщений и realtime у обеих копий общие.
Натив даёт UI через мост: chat token приложения, push-токен устройства, открытие обращения из push, выбор файлов, переход по ссылкам в браузер, состояние приложения (в фоне realtime отключается).
2. Требования
| Android | 8.0 (API 26) и новее |
| WebView | Android System WebView (Chromium) 111 и новее — цель сборки UI чата; с Google Play WebView обновляется сам. На устройствах без Google Play WebView обновляет производитель, и он может быть старше — тогда чат не открывается (см. ниже) |
| Сборка приложения | compileSdk 35+, Java 17 (байт-код SDK), Kotlin 2.1+ |
| Зависимости SDK | androidx.core 1.16, androidx.activity 1.10, androidx.webkit 1.18 (Apache-2.0); Firebase SDK не тянет — FCM подключает приложение |
| Инсталляция | публичный домен с /widget/mobile/ и public API, размещение приложения (авторизованная зона, «Интеграции → Мобильные приложения»), издатель chat token (TrustedIssuer) |
Устаревший WebView
При открытии чата SDK читает major-версию Chromium WebView (Chrome/<major> из user agent WebView, иначе — версия пакета WebViewCompat.getCurrentWebViewPackage). Если она ниже 111 или WebView не умеет WEB_MESSAGE_LISTENER, UI чата не загружается (UI kit на старом движке остался бы без стилей):
- экран чата показывает «Чат поддержки недоступен: обновите Android System WebView» без кнопки «Повторить»; пользователь закрывает его «Назад»;
- логгер приложения (
HelpdeskOptions.logger, §9) получаетERRORс кодомwebview.version_unsupported(WebView старше 111) илиwebview.outdated(нетWEB_MESSAGE_LISTENER) — по нему приложение может, например, предложить обновление WebView или другой канал поддержки; - версию прочитать не удалось — чат открывается как обычно, логгер получает
WARNwebview.version_unknown.
3. Подключение
AAR и POM лежат в офлайн-комплекте релиза: mobile/android/maven/ — каталог Maven. Скопируйте его в репозиторий приложения или в свой Maven.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("third-party/shtab-helpdesk-maven") }
}
}
// app/build.gradle.kts
dependencies {
implementation("ru.shtab.helpdesk:helpdesk-android:1.0.0-beta.2")
}
4. Настройка и открытие чата
class App : Application() {
override fun onCreate() {
super.onCreate()
HelpdeskSdk.configure(
this,
installationUrl = "https://support.example.com", // только origin, https
placementKey = "pk_…", // ключ приложения из консоли
options = HelpdeskOptions(appVersion = BuildConfig.VERSION_NAME),
)
HelpdeskSdk.setChatTokenProvider { callback ->
// Бэкенд приложения подписывает chat token для вошедшего пользователя:
// iss, aud = ключ размещения, sub, exp ≤ 10 мин, jti. Нет пользователя — null.
backend.issueSupportToken { token -> callback.onChatToken(token) }
}
}
}
// Кнопка «Поддержка»
HelpdeskSdk.openChat(activity)
- Провайдер вызывается при каждом открытии чата и когда сессия чата кончилась; отвечать можно с любого потока, один раз. Токен одноразовый, SDK его не хранит. Нет ответа за 15 с — чат открывается без входа и показывает «Войдите в приложении снова».
http://принимается только в debuggable-сборке (стенд), в release — исключениеIllegalArgumentException.- В приложении сессия чата начинается только с chat token приложения: cookie, оставшаяся в WebView от прежнего пользователя, сессию не возвращает.
HelpdeskEventListener(setEventListener) сообщает о непрочитанных в открытом чате и о том, что chat token приложения отклонён (onSessionExpired).
5. Выход и смена пользователя
При выходе пользователя из приложения вызовите HelpdeskSdk.logout(): экран чата закрывается, сессия завершается на сервере (сервер отзывает push этого устройства), cookie сессии и очередь неотправленных сообщений в WebView удаляются, обращение из push забывается. Нет сети — сервер узнает при следующем configure. Следующий пользователь входит своим chat token и не видит данных прежнего.
6. Push (FCM)
Сервер шлёт push без текста: в notification — «Новый ответ — Откройте чат поддержки, чтобы прочитать» на языке размещения, в data — плоские ключи conversation_id и kind (new_reply; тестовый из «Проверка приёма» — test без обращения). Проект Firebase, сервисный аккаунт и канал уведомлений задаются в консоли («Интеграции → Мобильные приложения → Android»).
class Messaging : FirebaseMessagingService() {
override fun onNewToken(token: String) {
Handler(Looper.getMainLooper()).post { HelpdeskSdk.registerPushToken(token) }
}
override fun onMessageReceived(message: RemoteMessage) {
// В фоне уведомление показывает FCM; в foreground — приложение.
if (HelpdeskNotifications.isHelpdeskPush(message.data)) {
HelpdeskNotifications.show(this, message.data, R.drawable.ic_notification,
message.notification?.title, message.notification?.body)
}
}
}
// Launcher-activity: тап по уведомлению приносит ключи push в intent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
HelpdeskSdk.handleNotification(intent, this) // true — push инсталляции, чат открыт
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
HelpdeskSdk.handleNotification(intent, this)
}
- Канал:
HelpdeskNotifications.createChannel(context, "Ответы поддержки")(idshtab_helpdesk, или свой — тот же, что в консоли). - Разрешение
POST_NOTIFICATIONS(Android 13+) приложение спрашивает само, с объяснением и кнопкой «Отложить» (как в sample). - Обращение из push открывается после входа пользователя чата; чужое обращение показывается как «Нет доступа к обращению».
- Push-токен хранится в SDK (Keystore) и регистрируется для клиента при каждом входе в чат: новый токен, полученный при закрытом чате, уходит на сервер при следующем открытии чата.
- Проверка без сервера: в debug-сборке
HelpdeskDebug.testPushData(context, conversationId)даёт тот жеdata, что шлёт сервер, аHelpdeskDebug.testNotificationIntent(...)— intent тапа. В release-сборке оба бросаютIllegalStateException.
7. Файлы
Скрепка в чате открывает системный выбор документов: изображения и PDF. Размер сверх лимита отклоняется до загрузки; лимит и типы организации проверяет сервер и объясняет отказ в чате. Файл передаётся странице по одноразовому handle, путь на устройстве UI не видит.
«Скачать» у файла в чате: UI отдаёт байты файла SDK по мосту, SDK сохраняет его сам. Android 10+ — в «Загрузки» (MediaStore, разрешений не нужно), с тостом «Файл сохранён в «Загрузки»». Android 8–9 — системное окно «Сохранить», место выбирает пользователь: SDK не добавляет приложению разрешения хранилища. Копия SDK лежит в cacheDir приложения, пока файл сохраняется, и удаляется с экраном чата.
8. Безопасность
| Что | Где |
|---|---|
| chat token приложения | не хранится: используется один раз |
| сессия чата | короткий bearer — в памяти страницы; cookie сессии (HttpOnly) — в хранилище cookie WebView, удаляется при logout |
| push-токен, обращение из push, отложенный logout | SharedPreferences ru.shtab.helpdesk, AES-256-GCM ключом Android Keystore |
| неотправленные сообщения | IndexedDB WebView (origin инсталляции), удаляется при logout |
- Ошибка TLS — отказ: SDK не переопределяет
onReceivedSslError, «продолжить всё равно» нет. - Мост принимает сообщения только из главного фрейма origin инсталляции; ссылки и любые переходы с него открываются в браузере.
- Значения SDK не нужны в резервной копии (ключ Keystore в неё не попадает, восстановленные значения не читаются и удаляются): исключите
ru.shtab.helpdesk.xmlизdataExtractionRules/fullBackupContent.
Certificate pinning (по умолчанию выключено)
HelpdeskOptions(certificatePins = setOf("sha256/<основной ключ>", "sha256/<резервный ключ>"))
Пины — SHA-256 SubjectPublicKeyInfo в base64 (ключ сервера или CA заказчика, всегда с резервным). С пинами SDK сам проверяет сертификат при чтении bridge.json, при logout и загружает страницу и ассеты UI своим соединением; не совпали — чат не открывается («Не удалось проверить сервер поддержки»).
Ограничение WebView. Запросы public API (fetch) и WebSocket realtime делает сам WebView: у Android WebView нет обработчика, где SDK мог бы проверить сертификат успешного TLS-соединения. Для них добавьте те же пины в Network Security Config приложения — распространяется ли он на WebView-трафик, ещё подтверждается на устройстве:
<network-security-config>
<domain-config>
<domain includeSubdomains="false">support.example.com</domain>
<pin-set expiration="2027-12-31">
<pin digest="SHA-256">основной ключ base64</pin>
<pin digest="SHA-256">резервный ключ base64</pin>
</pin-set>
</domain-config>
</network-security-config>
Остаточный риск. Пины SDK доказывают только то, что пиннованный ключ видит собственное соединение SDK. Обмен chat token и выдача bearer (POST /public/api/v1/session/*), остальные POST public API и WebSocket realtime проверяет TLS платформы. MITM, который отличает соединения WebView от соединений SDK (у них разный TLS-отпечаток) и пропускает последние, пинами SDK не обнаруживается. Поэтому обязательно:
- не добавляйте
<certificates src="user"/>вbase-configи вdomain-configдомена инсталляции — только в<debug-overrides>. Тогда приtargetSdk ≥ 24ни приложение, ни WebView не доверяют пользовательским CA, включая CA, поставленные MDM (они ложатся в пользовательское хранилище), и для перехвата нужен системный CA (root, прошивка) или неверно выпущенный публичный сертификат; - при включении пинов добавьте тот же
pin-setв Network Security Config (пример выше): действует ли он на трафик WebView, ещё подтверждается на устройстве — до подтверждения считайте POST и WebSocket чата защищёнными только доверием платформы; - устройства с root и пользовательскими прошивками этой защиты не дают; если это неприемлемо, проверяйте целостность устройства средствами приложения до открытия чата.
9. Диагностика
HelpdeskOptions(logger = HelpdeskLogger { level, code -> … }) — только коды, без текста, токенов и персональных данных; по умолчанию WARN/ERROR — в Logcat (тег ShtabHelpdesk). Основные коды:
| Код | Что значит |
|---|---|
ui.installation | открыт UI инсталляции |
ui.bundled.offline, …unreachable, …manifest_unavailable, …manifest_malformed, …bridge_unsupported, …page_failed, …no_handshake, …bridge_refused | открыт вшитый UI и почему |
tls.pin_mismatch | сертификат не совпал с пинами |
token.no_provider, token.timeout, token.provider_failed, token.malformed | chat token не получен |
ui.push.register_failed | сервер не принял push-токен (например, гость) |
logout.deferred | logout будет отправлен при следующем configure |
save.failed, save.busy, save.no_picker, save.out_of_order, save.wrong_size, save.refused, save.bad_piece, save.write_failed | файл из чата не сохранён: копия, окно «Сохранить» уже открыто или его нет, части от UI не сошлись |
webview.version_unsupported, webview.outdated | WebView старше 111 / без WEB_MESSAGE_LISTENER: чат не загружен, экран просит обновить Android System WebView (§2) |
webview.version_unknown | версию WebView не прочитать — чат открыт без проверки версии |
webview.renderer_gone | процесс WebView упал (экран закрывается, приложение живо) |