Клиентский SDK (iOS, Android, React Native)

Клиентский SDK приносит в ваше мобильное приложение тот же диалог и тот же in-app контент, которые получают посетители сайта. Чат идёт по уже существующему веб-каналу бота, поэтому операторы отвечают из того же инбокса, а in-app контент – это тот же документ, что и поп-ап на сайте, нацеленный на мобильные устройства.

Прочитайте это до планирования релиза. Регистрация и отзыв пуш-токена устройства работают полностью, но пуш на устройства SDK в продукте не отправляет никто: отправителя в APNs или FCM для клиентских устройств нет. SDK сообщает, что пуш выключен, и будет так сообщать, пока отправитель не появится. Всё остальное ниже – чат, контент, личность и офлайн-очередь – работает; мобильный пуш – нет.

Что делает SDK

  • Устанавливается – приложение регистрируется один раз, получает долговременный токен установки и хранит его в Keychain или Android Keystore.
  • Опознаёт пользователя – пока он анонимен, это один посетитель; после identify установка объединяется с опознанным человеком и делит с ним историю во всех остальных разделах MyBot.
  • Копит работу офлайн – события, свойства профиля, набор текста и операции с пуш-токеном записываются локально и уходят упорядоченными пачками, когда появляется сеть.
  • Ведёт чат – история, отправка, вложения, счётчик непрочитанного и курсор прочтения поверх того же диалога, с которым работают операторы.
  • Держит realtime – веб-сокет для новых, изменённых и удалённых сообщений, авторизованный одноразовым тикетом.
  • Показывает in-app контент – документы поп-апов из Контент-студии с квитанциями показа, действия и конверсии.

Платформы: iOS (Swift, iOS 15+, с фасадом для Objective-C и необязательным UI-таргетом), Android (Kotlin, minSdk 24, с фасадом для Java) и React Native (TypeScript поверх двух нативных SDK). Все три реализуют одну спецификацию и проверяются одним набором эталонных фикстур – см. «Контракт».

Включите проект

SDK настраивается для каждого бота на отдельном экране в кабинете. При включении вы указываете, каким приложениям он разрешён, и получаете две сущности.

Списки приложений. Вы перечисляете bundle id для iOS и имена пакетов для Android (хотя бы один нужен, чтобы создать проект, максимум по 64 в каждом списке). Приложению не из списка сервер отвечает ровно так же, как на неизвестный ключ проекта, поэтому по ответу нельзя отличить «неверный ключ» от «верный ключ, чужое приложение».

Ключ проекта выглядит как sdk1_eu_<24 hex>_<32 hex>. Он публичный, как install-ключ виджета: попадает в сборку приложения, это нормально, и он не даёт читать диалоги или менять настройки. Регион внутри нужен, чтобы кабинет показывал, к какому развёртыванию относится проект. SDK передаёт ключ дословно и никогда его не разбирает.

Секрет личности – совсем другая сущность, и в приложение он не попадает никогда. Он показывается ровно один раз – при включении проекта и затем при каждой ротации. Скопируйте его в этот момент в хранилище секретов своего бэкенда: ни кабинет, ни API больше его не покажут.

Ротация секрета личности создаёт новый и оставляет предыдущий рабочим ещё сутки, чтобы у вас был цикл выкатки. Ничто из уже установленного ротацией не ломается: ключ проекта не меняется, устройства продолжают работать.

Отзыв устройства. В списке установок видны платформа, идентификатор приложения, версия SDK, локаль, часовой пояс, признак опознанного пользователя и время последней активности (до 200 недавних записей). Удаление записи немедленно лишает эту установку доступа.

Замечание по развёртыванию. Создание проекта требует общего для развёртывания секрета CUSTOMER_SDK_KEY_SECRET, которым подписываются ключи проектов. Продовая ячейка без него не стартует. Там, где его нет, включение отвечает 503, а всё остальное продолжает работать – это пробел конфигурации, а не поломка функции. Ротация секрета личности его не использует и работает в любом случае.

Установка и запуск клиента

Исходники SDK лежат в мобильном репозитории в каталоге customer-sdk/: Swift-пакет MyBotSDK (customer-sdk/ios), Gradle-модуль mybot-sdk в пространстве имён dev.gelfand.mybot.sdk (customer-sdk/android) и npm-пакет @mybot/customer-sdk-react-native (customer-sdk/react-native).

Для запуска клиента нужны ключ проекта, базовый адрес вашего развёртывания MyBot и идентификатор приложения. Хост у сборки один: SDK не выбирает другой адрес по региону из ключа.

let client = try await MyBotClient.start(
    configuration: Configuration(
        projectKey: "sdk1_eu_…",
        baseURL: URL(string: "https://api.getmybot.dev")!,
        appIdentifier: "dev.getmybot.example",
        sdkVersion: "1.0.0"
    )
)
val client = MyBotClient.start(
    context,
    MyBotConfiguration(
        projectKey = "sdk1_eu_…",
        baseUrl = "https://api.getmybot.dev",
    ),
)
const client = await MyBotClient.start({
  projectKey: "sdk1_eu_…",
  baseURL: "https://api.getmybot.dev",
  appIdentifier: "dev.getmybot.example",
});

Первый запуск регистрирует установку и сохраняет её токен, все следующие – продолжают её. Токен выдаётся один раз; если он потерян или есть подозрение на утечку, SDK обновляет его на месте, не меняя личность.

Личность: подписывает ваш бэкенд, а не устройство

Устройство не имеет права само заявлять, кто его пользователь. Опознание – это утверждение, подписанное вашим собственным бэкендом секретом личности, а приложение лишь передаёт непрозрачный результат:

  1. Приложение спрашивает у вашего бэкенда, кто оно.
  2. Бэкенд, который уже аутентифицировал пользователя, выпускает короткоживущий JWT, подписанный секретом личности.
  3. Приложение передаёт эту строку в identify. SDK её не разбирает.

Сервер проверяет утверждение строго, и любое отклонение отвергает его целиком:

{
  "alg": "HS256",
  "project_key": "sdk1_eu_…",
  "aud": "mybot-customer-sdk",
  "jti": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "iat": 1730000000,
  "exp": 1730000120,
  "user_id": "acct_10293",
  "properties": { "plan": "pro", "role": "admin" }
}
  • Алгоритм строго HS256, аудитория строго mybot-customer-sdk, полей iss, sub и nbf быть не должно.
  • jti – канонический UUIDv4, iat не в будущем, время жизни exp - iatне более 300 секунд.
  • user_id – ваш непрозрачный стабильный идентификатор (1–256 байт), а не сырой email или телефон, введённый в форму на устройстве.
  • properties необязательны, не более 16 записей, только из закрытого словаря account_id, plan, role, locale со строковыми значениями до 256 байт.

Повторный identify для того же пользователя идемпотентен, для другого – переносит установку к нему. Сырые контактные данные через этот API не проходят: тот же закрытый словарь действует и для маршрута профиля, и для свойств из очереди.

Выход из аккаунта поднимает эпоху личности

У каждой установки есть эпоха – целое число, которое начинается с 1 и растёт на единицу при каждом выходе. Она проверяется на каждом запросе без окна снисхождения, и именно она делает выход безопасным на общем телефоне:

  • операции прежней эпохи из очереди отбрасываются и больше никогда не будут приняты: клиент их удаляет, а сервер всё равно отверг бы пачку целиком;
  • локальный счётчик последовательности начинается заново с 1;
  • открытый realtime-сокет закрывается сразу, а тикет, выпущенный до выхода, погасить уже нельзя;
  • незавершённые отправки со старой эпохой отменяются;
  • пуш-токены, зарегистрированные в прежней эпохе, отзывает сам сервер, в той же транзакции, где поднимается эпоха, – приложению не нужно помнить об этом при выходе;
  • кэши, привязанные к прежнему человеку (назначения контента, бейдж непрочитанного, сохранённые поля профиля), очищаются.

Про realtime стоит сказать отдельно: рукопожатие веб-сокета не может нести заголовок эпохи, который несёт обычный запрос. SDK получает одноразовый тикет по HTTPS, а в момент погашения сервер заново читает эпоху установки из базы и сравнивает её с эпохой, зафиксированной при выпуске тикета. Выход, случившийся в промежутке, отменяет апгрейд, даже если сам тикет ещё не использован. Именно поэтому вышедший из аккаунта клиент не попадёт в чужой диалог.

Обновление токена – тихий родственник выхода: та же эпоха, тот же человек, меняется только токен, и ни один из перечисленных эффектов не срабатывает.

Офлайн-очередь

Операции, записанные без сети, уходят упорядоченными пачками. Каждая пачка несёт клиентский идентификатор, эпоху и непрерывный диапазон номеров.

ОперацияЧто делает
track_eventзаписывает продуктовое событие (на практике имя должно подходить под ^[a-z][a-z0-9_.]{0,63}$ – действует более строгая из двух проверок)
set_propertiesзадаёт свойства профиля из закрытого словаря account_id, plan, role, locale
register_push_tokenсохраняет токен APNs или FCM в зашифрованном виде (sandbox или production)
revoke_push_tokenотзывает токен; отзыв неизвестного токена – молчаливая успешная операция, а не ошибка
notification_openedаналитика открытия уведомления, best effort
in_app_impression, in_app_actionквитанции по одному назначению in-app контента
typingиндикация набора текста в чате

Правила на сервере:

  • 1–100 операций в пачке, номера продолжаются ровно с того места, где закончилась последняя принятая пачка. Разрыв или наложение отвергают пачку целиком.
  • Пачка с идентификатором, уже принятым для этой установки и эпохи, отвечает «дубликат» и ничего не выполняет заново, поэтому повтор после потерянного ответа безопасен.
  • Неверная эпоха отвергает пачку целиком, а всё остальное сообщается по каждой операции кодом из закрытого списка: одно плохое свойство не отбрасывает пачку хороших событий.

Локальные границы одинаковы на всех трёх платформах, чтобы телефон, неделями пробывший офлайн, не рос бесконечно. Первыми вытесняются самые старые ожидающие операции:

  • 2000 операций в очереди на установку;
  • 2 МиБ сериализованного хранилища очереди;
  • 7 дней возраста – независимо от двух предыдущих границ.

Операция, которая прямо сейчас отправляется, не вытесняется никогда. На каждое вытеснение срабатывает диагностический колбэк с локальным идентификатором и причиной (количество, объём или возраст) – без содержимого операции. Неудачные отправки повторяются от базы в 30 секунд с удвоением до 240 секунд и полным разбросом; отказ авторизации не повторяется никогда, потому что это заслон, а не сбой.

Чат

Чат – это веб-канал бота, увиденный с телефона: те же сообщения, тот же инбокс оператора, те же триггеры автоматизации при отправке.

  • История листается вперёд от курсора, до 50 сообщений на страницу. Поле direction указано относительно клиента: in – от оператора или бота.
  • Отправка принимает клиентский идентификатор сообщения и отвечает «поставлено в очередь». Это значит «принято к доставке», а не «доставлено»: реальное состояние придёт через realtime или при следующем чтении истории. Повтор с тем же идентификатором и тем же текстом ничего не делает, повтор с другим текстом – конфликт и признак ошибки в приложении.
  • Вложения загружаются по одному файлу, до 10 МиБ, дедуплицируются по хешу содержимого внутри бота и возвращаются готовой ссылкой.
  • Непрочитанное считается для каждой установки по её собственному курсору, отдельно от прочтений оператора, и курсор движется только вперёд.
  • Realtime передаёт новые, изменённые и удалённые сообщения и набор текста. При подключении сервер отдаёт пропущенное за время офлайна (до 50 кадров), а затем стримит живые события; курсора переподключения со стороны клиента нет.

Если владелец бота не подключал веб-канал, все маршруты чата отвечают, что чат для этого бота недоступен. Показывайте это как выключенную функцию, а не как временную ошибку.

In-app контент

GET /sdk/v1/content возвращает уже назначенные элементы: тип, приоритет, задержку, правила по визитам и частоте, блоки и оформление. Пустой список – нормальный ответ.

Кому что показывать, решает таргетинг. Поп-ап, у которого в таргетинге по устройствам нет мобильных, в SDK не попадёт никогда; поп-ап вообще без таргетинга по устройствам считается «для всех устройств» и попадёт. Вариант назначается детерминированно для конкретного человека, поэтому анонимный пользователь видит один и тот же вариант, пока identify или выход из аккаунта не пересчитают распределение заново.

Квитанции бывают трёх видов: impression, action и conversion. Сообщение, которое пользователь смахнул, – клиентское конечное состояние без серверного события, ровно как у закрытого поп-апа на сайте.

Мобильные пуш-токены

register_push_token и revoke_push_token работают полностью: токен хранится зашифрованным вместе с отпечатком для поиска, отзыв неизвестного токена молчит, чтобы им нельзя было пользоваться как оракулом, и оба участвуют в выгрузке и удалении данных.

Доставленного уведомления из этого всё равно не получается. Отправителя на клиентские устройства в продукте нет; операторский пуш в кабинете – другая система с другим хранилищем, другими ключами и другой аудиторией. Поэтому конфигурация SDK сообщает, что пуш выключен, и это значение намеренно закреплено фикстурами контракта, чтобы никто не «починил» его раньше появления отправителя. Одно следствие стоит учесть: у вышедшего из аккаунта устройства зарегистрированный токен сохраняется, потому что токены не отсекаются эпохой, – отзывайте его из приложения при выходе пользователя.

Ошибки

Любая ошибка /sdk/v1 – это JSON-конверт с одним полем, и SDK классифицирует её по HTTP-статусу:

СтатусЧто означаетЧто делает SDK
400неверный локальный вводне повторяет, показывает как ошибку клиента
401заслон авторизации: устаревшая эпоха, отозванный токен, неизвестный проект, использованный тикетне повторяет с тем же credential
404неизвестное или чужое назначение контентаудаляет как навсегда недействительное
409чат недоступен для бота либо конфликт идемпотентностипоказывает, не повторяет
413вложение больше 10 МиБпоказывает до любых повторов
500, 503временная ошибкаповторяет с выдержкой для идемпотентных операций

Приватность и диагностика

Ни один маршрут SDK не возвращает сырые контактные данные, а словарь свойств закрыт от начала до конца. Диагностика выключена по умолчанию; включённая, она несёт только код события из закрытого списка, метку времени и очищенные числовые метаданные – никогда не токен, утверждение о личности, пуш-токен, ссылку и не содержимое сообщений, свойств или контента. Установки SDK относятся к категории хранения sdk_diagnostics со сроком по умолчанию 180 дней и участвуют в объединении подписчиков, выгрузке и удалении данных.

Контракт

Проводной интерфейс SDK – намеренно отдельный контракт от REST API: личный токен никогда не принимается на /sdk/v1, а токен установки – на /api. Его документ OpenAPI сервер публикует сам, без авторизации:

GET https://ваше-развёртывание/sdk/openapi.yaml

Помимо маршрутов есть поведение, которое все клиенты обязаны реализовать одинаково: конечные автоматы, порядок операций, границы очереди, выдержки, классификация ошибок и места, где спецификация намеренно следует за сервером. Оно описано в файле customer-sdk/contract/behavior.md в мобильном репозитории вместе с машинно снятым слепком серверных структур, перечислений, лимитов и шаблонов и 75 эталонными фикстурами. Скрипт заново выводит этот слепок из копии бэкенда и падает при незадекларированном расхождении, а тесты каждой платформы читают те же фикстуры – поэтому изменение на сервере не может тихо рассинхронизировать три клиента.

API и MCP

Семь владельческих маршрутов управляют проектом по личному токену – см. REST API и токены. Для чтения нужен скоуп customer_sdk:read и право на аналитику у бота, для записи – customer_sdk:write и право на реакции. В MCP есть два инструмента только на чтение и намеренно нет инструментов включения, ротации и отзыва – см. MCP.

Что дальше