Ассистент в Android и iOS SDK
Этот гайд проводит от настройки кампаний до работающей кнопки ассистента в приложении. После подключения покупатель открывает полноэкранный чат, задаёт вопрос, видит подборку товаров в дизайне вашего приложения и переходит из неё в карточку товара.
Вам нужно сделать три части интерфейса: кнопку входа, товарную карусель и обработчик переходов. Экран чата, отправку сообщений, хранение текущего диалога и отправку событий ассистента берёт на себя Gravity SDK.
С чего начать
Если Gravity SDK уже установлен, начните с
Шаг 1. Подготовьте SDK и раздел магазина
До написания кода ассистента проверьте следующие условия. Настройки кабинета может подготовить ваша команда маркетинга или менеджер Gravity Field; разработчику нужны их значения и опубликованные кампании.
Отдельно задавать пользователя для чата не нужно. Используйте обычную идентификацию Gravity SDK. Правила обмена section, uid и threadId описаны
Шаг 2. Настройте две API-кампании
Кампания возвращает настройки интерфейса, которые SDK получает по selector — строковому имени кампании. В этой интеграции используются два разных selector:
Имена в таблице — пример. Задайте свои имена в кабинете и передайте те же строки в приложение, без изменения регистра или пробелов.
Создайте кампании в кабинете
Для каждой строки таблицы:
- Откройте Campaigns → API Campaigns и создайте кампанию типа Custom JSON.
- Укажите её API-селектор.
- Добавьте Experience — сценарий с условиями показа. Для первого теста выберите условия, которым соответствует ваш тестовый пользователь и экран.
- Создайте вариацию и вставьте подходящий JSON из примеров ниже в редактор её содержимого.
- Активируйте и сценарий, и кампанию, затем нажмите Опубликовать.
Пошаговые экраны кабинета: Создание API-кампании. Если терминология незнакома: кампания → сценарий → вариация. Условия выбора настраиваются в таргетинге и расписании; редактируемые тексты — через переменные вариаций.
Одного сохранения JSON недостаточно
Неопубликованная кампания, неактивный сценарий или несовпадающий таргетинг могут дать пустой ответ. Проверьте публикацию обеих кампаний для того PageContext, с которым тестируете приложение.
JSON кнопки входа
Для assistant-entry:
{ "enabled": true, "title": "Спросите помощника" }
enabled: trueразрешает приложению подготовить ассистента на этом экране.title— подпись кнопки приложения. Её положение, иконку и внешний вид задаёте вы.- При
enabled: false, отсутствии данных, некорректном JSON или ошибке запроса кнопка остаётся скрытой.
JSON ассистента
Для shopping-assistant:
{
"enabled": true,
"title": "Помощник магазина",
"subtitle": "Помогу выбрать подходящий товар.",
"suggests": ["Помогите выбрать подарок"]
}
При enabled: false остальные поля не используются, результат подготовки — unavailable(disabled). Неизвестные поля игнорируются. Несколько разных конфигураций для одного selector — invalid_configuration.
В редактор вставляется JSON-объект из примера, без внешней обёртки custom. В ответе SDK этот JSON находится строкой в CampaignContent.custom.json. Стартовые подсказки приходят из этой кампании; подсказки после ответа — из response.ui.suggests. Подготовка не отправляет скрытое сообщение ассистенту.
Шаг 3. Подключите товарный блок и переходы
До первой подготовки зарегистрируйте два объекта приложения:
GravitySDK.instance.setAssistantProductBlockBuilder(productBlockBuilder)
GravitySDK.instance.setAssistantActionHandler(actionHandler)
GravitySDK.instance.setAssistantProductBlockBuilder(productBlockBuilder)
GravitySDK.instance.setAssistantActionHandler(actionHandler)
Здесь productBlockBuilder и actionHandler — ваши реализации интерфейсов, приведённых ниже. В builder можно использовать существующий компонент карточки магазина. Он получает товары от SDK: отдельный запрос каталога для заполнения карусели не требуется.
При нажатии карточка сообщает tap SDK. Затем SDK учитывает клик, закрывает чат и вызывает ваш handler. В handler выполните переход по SKU или URL. Не делайте второй переход прямо из карточки. Для поддержки обработайте OpenSupportChat, для ссылки — FollowUrl; неподдержанное действие возвращает false из canHandle.
Что SDK делает сам
SDK создаёт экран чата, показывает ввод и ответы, управляет вертикальной прокруткой и кнопкой «Новый чат». Ваша карусель управляет только горизонтальной прокруткой. При отсутствии builder или handler подготовка возвращает invalid_configuration.
Шаг 4. Покажите кнопку после подготовки
На каждом экране, где нужен ассистент:
- Сразу скройте кнопку предыдущего экрана и передайте
trackView(pageContext). - Вызовите
getContentBySelector("assistant-entry", pageContext). - Прочитайте JSON точки входа. Если она включена, вызовите
prepareAssistant("shopping-assistant", pageContext). - Только после
readyдля текущего экрана покажите кнопку сentry.title. - При нажатии вызовите
openAssistantс видимой Activity или UIViewController.
PageContext должен описывать экран, с которого открывается чат: например, PRODUCT и SKU для карточки товара или CATEGORY и данные категории для каталога. Поля и примеры: PageContext Android, PageContext iOS, правила контекста API V2. Не меняйте регистр SKU, категорий и региона относительно фида.
prepareAssistant не заменяет trackView и не отправляет его повторно. Получение данных кампании само по себе не показывает кнопку: отрисовка точки входа остаётся в приложении.
Как прочитать настройку точки входа
Ответ getContentBySelector содержит выбранный контент по пути:
ContentResponse.data[]
→ campaign.payload[]
→ variation.contents[]
→ content.custom.json // строка с JSON, которую нужно разобрать
Для точки входа используйте один выбранный контент своей кампании. Не объединяйте вариации разных кампаний. parseEntry в примере ниже — функция приложения: она разбирает эту строку и возвращает enabled и title; отсутствие или ошибка разбора означает, что кнопка не показывается. JSON второго selector приложение не разбирает — это делает prepareAssistant.
Полный порядок с защитой от смены экрана
Это псевдокод управления экраном. generation, задача экрана и видимость кнопки принадлежат приложению. В Kotlin используйте lifecycleScope/Job, в Swift — Task на MainActor. Поколение меняется до запроса точки входа, а не только перед подготовкой.
entrySelector = "assistant-entry"
assistantSelector = "shopping-assistant"
onScreenShown(pageContext):
generation += 1
cancel(previousScreenTask)
myGeneration = generation
hideEntryButton()
sdk.trackView(pageContext)
previousScreenTask = launch:
entryResponse = await sdk.getContentBySelector(entrySelector, pageContext)
if taskCancelled or myGeneration != generation: return
entry = parseEntry(entryResponse) // custom.json; ошибка/нет данных -> disabled
if not entry.enabled: return
result = await sdk.prepareAssistant(assistantSelector, pageContext, timeout = 10s)
if taskCancelled or myGeneration != generation: return
if result == ready: showEntryButton(entry.title)
onScreenHiddenOrIdentityChanged:
generation += 1
cancel(previousScreenTask)
hideEntryButton()
onEntryTap:
result = sdk.openAssistant(currentPresenter)
if result == not_ready or result == invalid_presenter:
hideEntryButton()
Любая ошибка обоих запросов оставляет кнопку скрытой. Отмена coroutine сохраняет стандартную семантику Kotlin CancellationException; не перехватывайте её как сетевую ошибку. Swift Task дополнительно проверяет Task.isCancelled. currentPresenter — видимая ComponentActivity либо UIViewController, готовый к презентации.
Шаг 5. Проверьте первый запуск
Если кнопка или чат не появились
Для проверки запросов платформы используйте API-логи. Все результаты и ошибки разобраны
Дополнительные сценарии перед выпуском
- При отключённой или отсутствующей кампании кнопки нет; при медленной подготовке не появляется неработающая кнопка.
- Быстрые переходы между страницами не показывают кнопку или подсказки предыдущей страницы.
- Ошибка повторной подготовки не удаляет активный диалог; «Новый чат» не показывает старый стартовый экран.
- Поворот, большие шрифты и пустые подборки не создают нулевую или бесконечную высоту блока.
- Карточки за пределами горизонтального viewport не дают impression; перерисовки не создают дубли.
- SKU без URL открывается; товар без обоих остаётся видимым без нажатия.
- Товар и кнопка поддержки закрывают чат до перехода; быстрый двойной tap не открывает два экрана.
- Чужая блокировка UI-кампаний сохраняется после закрытия ассистента.
- После смены пользователя не видны старые сообщения, товары и поздние ответы.
Методы SDK
Сигнатуры для регистрации компонентов, подготовки и управления экраном ассистента.
Методы вызываются у GravitySDK.instance. Асинхронная подготовка — suspend-метод; действия с UI выполняются в главном потоке.
suspend fun prepareAssistant(
assistantSelector: String,
pageContext: PageContext,
timeoutMillis: Long = 10_000,
): AssistantPrepareResult
fun openAssistant(presenter: ComponentActivity): AssistantOpenResult
fun closeAssistant()
fun setAssistantProductBlockBuilder(builder: AssistantProductBlockBuilder)
fun setAssistantActionHandler(handler: AssistantActionHandler)
fun performAssistantProductAction(
product: AssistantProductRef,
action: AssistantProductAction,
): AssistantInteractionResult
Методы доступны у GravitySDK.instance. Подготовка использует Swift concurrency; UI API и обработчики работают на MainActor.
@MainActor
func prepareAssistant(
assistantSelector: String,
pageContext: PageContext,
timeout: TimeInterval = 10
) async -> AssistantPrepareResult
@MainActor func openAssistant(presenter: UIViewController) -> AssistantOpenResult
@MainActor func closeAssistant()
@MainActor func setAssistantProductBlockBuilder(_ builder: any AssistantProductBlockBuilder)
@MainActor func setAssistantActionHandler(_ handler: any AssistantActionHandler)
@MainActor func performAssistantProductAction(
product: AssistantProductRef,
action: AssistantProductAction
) -> AssistantInteractionResult
Товарный блок приложения
Builder получает целую подборку. SDK управляет вертикальной прокруткой чата; приложение рисует заголовок, карточки и горизонтальную прокрутку.
interface AssistantProductBlockBuilder {
@Composable
fun Build(block: AssistantProductBlock, layout: AssistantProductLayout)
}
data class AssistantProductLayout(
val availableWidth: Dp,
val isInteractionEnabled: Boolean,
)
@MainActor
protocol AssistantProductBlockBuilder {
func build(block: AssistantProductBlock, layout: AssistantProductLayout) -> AnyView
}
struct AssistantProductLayout {
let availableWidth: CGFloat
let isInteractionEnabled: Bool
}
availableWidth — полезная ширина внутри чата после safe area и внутренних отступов, в dp на Android и points на iOS. Блок занимает ровно эту ширину и возвращает конечную измеряемую высоту по содержимому. Не используйте бесконечную высоту, fillMaxHeight, вертикальный ScrollView или GeometryReader как единственный источник высоты.
На Android корневому контейнеру задают Modifier.width(layout.availableWidth); Compose измеряет высоту содержимого. На iOS применяют .frame(width: layout.availableWidth) и .fixedSize(horizontal: false, vertical: true); SwiftUI сообщает высоту контейнеру SDK. При повороте, изменении размера окна, масштаба текста или загрузке изображения layout пересчитывается. Используйте соотношение сторон/placeholder для изображения, чтобы высота была определена ещё до загрузки. Для UIKit/Android View обёртка должна участвовать в нативном измерении высоты.
Модель данных
Поля товара, кроме itemId, ref, name и canOpen, могут отсутствовать. Не удаляйте товар из-за отсутствующей картинки, цены или категории. Показывайте placeholder и форматируйте цену по правилам магазина. Пустой блок показывает пустое состояние. События аналитики хранятся внутри SDK и не требуются builder.
Видимость и нажатия
Приложение вызывает публичный метод SDK; это не callback, который SDK вызывает в приложении:
GravitySDK.instance.performAssistantProductAction(
product = product.ref,
action = AssistantProductAction.VisibleImpression,
)
GravitySDK.instance.performAssistantProductAction(
product = product.ref,
action = AssistantProductAction.Tap,
)
GravitySDK.instance.performAssistantProductAction(product: product.ref, action: .visibleImpression)
GravitySDK.instance.performAssistantProductAction(product: product.ref, action: .tap)
AssistantProductAction имеет только visibleImpression и tap. AssistantInteractionResult: accepted, duplicate, stale_reference, not_actionable. На Kotlin это VisibleImpression, Tap, Accepted, Duplicate, StaleReference, NotActionable; в Swift — одноимённые case в lowerCamelCase.
- Отправляйте
visibleImpression, когда не менее 50% площади карточки видно непрерывно одну секунду одновременно внутри карусели, видимой области чата и окна приложения. Фоновое приложение и закрытый чат не считаются видимостью. При потере видимости таймер сбрасывают. - Отправляйте
tapодин раз на физическое нажатие, только приlayout.isInteractionEnabled && product.canOpen. - Builder обязан применить это условие к кликам и accessibility actions. SDK не может изменить обработчики произвольной host view. SDK дополнительно отклоняет недопустимое действие на своей границе.
- Повторное создание view не создаёт новые показы: SDK принимает один
visibleImpressionдля одного появления товара за одно открытие чата. При новом открытии выдаются новыеref. - Старые ссылки после закрытия, нового чата или смены идентичности возвращают
stale_referenceбез tracking и навигации. Повторный tap в уже начатом переходе возвращаетduplicate.
Действия и навигация
Единый AssistantActionHandler обслуживает и товары, и действия ответа: например, open_support_chat. Он должен быть зарегистрирован до подготовки. Отсутствие handler или builder — invalid_configuration, поэтому интеграция не доходит до чата с неработающими товарами.
interface AssistantActionHandler {
fun canHandle(action: AssistantAction): Boolean
fun handle(action: AssistantAction)
}
sealed interface AssistantAction {
data class OpenProduct(val product: AssistantProduct) : AssistantAction
data class OpenSupportChat(val payload: Map<String, JsonValue>) : AssistantAction
data class FollowUrl(val url: String) : AssistantAction
data class Custom(val type: String, val payload: Map<String, JsonValue>) : AssistantAction
}
@MainActor
protocol AssistantActionHandler {
func canHandle(_ action: AssistantAction) -> Bool
func handle(_ action: AssistantAction)
}
enum AssistantAction {
case openProduct(AssistantProduct)
case openSupportChat(payload: [String: JSONValue])
case followUrl(URL)
case custom(type: String, payload: [String: JSONValue])
}
JsonValue/JSONValue — JSON-значение: null, boolean, number, string, array или object. payload содержит исходный объект серверной кнопки (type, value, action и прочие полученные поля) без переименования ключей; отдельного серверного поля payload не требуется. Серверное action: "follow_url" с корректным HTTP(S) URL преобразуется в FollowUrl/.followUrl; open_support_chat — в OpenSupportChat/.openSupportChat. canHandle синхронен, не выполняет сеть и не запускает навигацию. Для неизвестного типа возвращайте false; SDK оставляет такое действие недоступным и фиксирует диагностику, не ломая весь ответ.
Для товара SDK проверяет действие, ставит click tracking на отправку, закрывает чат и после завершения закрытия вызывает handle. Для open_support_chat, follow_url и поддержанных custom-действий порядок закрытия и вызова такой же. Сам переход — ответственность приложения. При обработке tap не выполняйте второй переход напрямую из builder и не отправляйте отдельный sendProductEngagement.
В handle откройте карточку по SKU либо URL, чат поддержки либо другой экран приложения. Обработчик выполняется один раз для принятого нажатия. Асинхронная аналитика не задерживает навигацию. Если приложение не может завершить переход после передачи управления, оно показывает собственную ошибку навигации.
Подготовка, контекст и диалог
Подготовка и диалог — независимые состояния. Подготовка бывает idle, preparing, ready, unavailable или failed. Диалог бывает empty, active или sending. Подготовка никогда не отправляет скрытое сообщение от имени пользователя.
Каждая новая подготовка сразу делает предыдущий стартовый экран недействительным. Даже при равных аргументах действует последний вызов. Поздний ответ, отмена или timeout старого запроса не меняют результат нового.
Для кнопки приложения достаточно результата подготовки; отдельный hasAssistantActiveDialog не нужен. При каждой новой странице кнопку показывают после ready, даже если в памяти есть диалог. Уже открытый чат при этом не закрывается из-за обычной ошибки подготовки.
Контекст активного диалога фиксируется при его старте. Переход на другую страницу не переписывает контекст незавершённого запроса. Новый контекст применяется к следующему новому чату. Значения SKU, категорий и региона передаются без изменения регистра, согласно PageContext Android и PageContext iOS.
SDK не допускает параллельной отправки двух сообщений в одном диалоге и блокирует повторное нажатие отправки. Во время запроса виден индикатор. Таймаут запроса ответа — 120 секунд; он независим от десятисекундной подготовки. При сетевой ошибке текст сохраняется; SDK не отправляет его автоматически повторно. Если ответ сервера мог быть сохранён до обрыва связи, SDK сначала проверяет историю текущего диалога. Повтор использует тот же ID сообщения и исходный запрос, включая контекст; отредактированный текст является новым сообщением. Повтор после неопределённого сетевого исхода не даёт серверной гарантии exactly-once.
Для сообщений message_conflict соответствует HTTP 409 MESSAGE_ID_CONFLICT: SDK не создаёт новый ID автоматически и не перезаписывает ответ. Для истории history_unavailable соответствует HTTP 404 SHOPPING_HISTORY_NOT_FOUND: это не разрешение стереть локальные сообщения. Ошибки истории или генерации сохраняют текущий диалог. Остальные сетевые ошибки используют те же коды, что перечислены выше. Оценки ответа отправляет SDK по assistantMessageId из генерации либо id сообщения ассистента из истории; приложение не отправляет их повторно. Пока корректного ID ответа нет, отправка оценки недоступна.
Закрытие UI не отменяет уже отправленное сообщение: успешный ответ продолжает обрабатываться в текущем диалоге. «Новый чат», смена идентичности и смена ассистента инвалидируют старые ответы; они не попадают в новый диалог. Уже отправленный запрос может завершиться и сохраниться в старом серверном диалоге.
Результаты и ошибки
Имена результатов ниже записаны в общем формате. В Kotlin используются Ready, Unavailable(reason), Failure(error), Cancelled, Superseded; в Swift — .ready, .unavailable(reason), .failure(error), .cancelled, .superseded. Результаты подготовки и их вложенные типы сравнимы по значению (Equatable в Swift).
AssistantError содержит code, диагностическое message и необязательный httpStatus. Не показывайте диагностический текст пользователю напрямую.
Опечатка в непустом selector, давшая пустой ответ, — unavailable, а не доказанная invalid_configuration. ready подтверждает готовность стартового экрана; доступность следующего запроса сообщения проверяется при его отправке.
AssistantOpenResult: opened, already_open, not_ready, invalid_presenter. Повторное открытие не создаёт второй экран. invalid_presenter означает, что Activity/UIViewController уже закрыт, не виден или занят несовместимой презентацией.
Аналитика
События жизненного цикла SDK отправляет через обычный канал custom events Gravity Field. Приложение не дублирует их:
События содержат идентификаторы диалога, сообщения и кампании, где применимо, без текста переписки. SDK сохраняет привязку события к исходному пользователю; событие старого диалога не отправляется от имени нового аккаунта. Завершение процесса не гарантирует доставку chat_dialog_close или chat_dialog_end.
SDK использует события click и visible_impression из product.events ответа Shopping API. Для каждого принятого действия он отправляет все допустимые URL соответствующего типа; одинаковые URL внутри одного действия отправляются один раз. Отсутствие событий не блокирует отображение товара и навигацию, но аналитика этого действия через данный механизм отсутствует.
Приложение не вызывает tracking URL, не создаёт их из SKU и не отправляет одновременно обычный sendProductEngagement. События товаров ассистента и engagement обычных кампаний — разные механизмы. События уровня ответа не подменяют показы конкретных карточек. SDK обрабатывает поддержанные события ответа отдельно при соответствующем событии жизненного цикла ответа.
Очередь уже принятых кликов не отменяется закрытием чата; событие сохраняет исходные товар и идентичность. SDK не обещает доставку после завершения процесса приложения или exactly-once доставку по сети.
Кнопки open_support_chat и follow_url сами по себе не содержат товарных tracking URL. Вызов action handler не означает отправку product click и не подменяет аналитику приложения для этих действий.
Другие UI-кампании
На время презентации и показа ассистента SDK блокирует автоматические modal/in-app показы и переходы шагов таких кампаний. Блокировка устанавливается до открытия и снимается после полного закрытия, включая системный Back и swipe-dismiss.
Ассистент снимает только собственную блокировку. Если приложение уже вызвало lockPresentation, закрытие чата не отменяет блокировку приложения. Ошибка открытия и уничтожение экрана также освобождают только блокировку ассистента.
Получение кампаний и отправка событий продолжаются. Подавленные кампании автоматически после закрытия чата не воспроизводятся; следующие показы определяются последующими обычными событиями приложения. Inline-контент на экранах приложения не становится модальным и не блокируется этим правилом.
Подключение к Shopping API
SDK использует https://shopping-assistant-api.gravityfield.ai. runtimeContext.tenant_id равен section, указанному при инициализации Gravity SDK. Этот раздел должен быть подключён к Shopping Assistant; selector выбирает сценарий внутри раздела и не переключает tenant. resourceId и runtimeContext.uid — канонический user.uid Gravity Field, полученный базовым SDK. Собственный ID из setUser не подставляется вместо него напрямую.
Подготовка дожидается готовности базового SDK и разрешения user.uid в пределах своего общего timeout. До этого ready не возвращается; случайный локальный ID вместо user.uid не создаётся. threadId — отдельный UUID чата, а не user.ses.
Выбор кампании идёт через обычный Gravity API V2 с ключом базового SDK. Запросы к публичному Shopping API передают Content-Type: application/json; ключ API V2 и серверные ключи коннекторов в эти запросы и в custom.json не добавляют. Отдельный логин пользователя в ассистент не нужен. На стороне сервиса раздел должен допускать обращения из мобильной сети; HTTP 403, в том числе IP_NOT_ALLOWED, означает отказ в доступе и возвращается как unauthorized. Настройки доступа не меняются методами SDK.
Подробный транспортный контракт: интеграция Shopping Assistant по API. Приложение не вызывает эти запросы параллельно с SDK.
Связанные разделы