# SDK Gravity Field (Flutter)

Gravity Field SDK — это лёгкий клиент для интеграции мобильных приложений (iOS / Android) с платформой персонализации и A/B-тестирования. Он позволяет запускать кампании без необходимости реализовывать собственную логику таргетинга, выбора и аналитики.

SDK работает по принципу "тонкого клиента": все решения принимает сервер (бекенд Gravity Field), а SDK:

  • передаёт контекст текущего экрана,
  • получает кампании и их содержимое в виде JSON или встроенных шаблонов,
  • активирует показ inline и in-app кампаний
  • трекает взаимодействия пользователя (просмотры, клики, покупки и т.д.),
  • и помогает фиксировать конверсии для аналитики и обучения моделей.

📦 Эта документация предназначена для мобильных разработчиков и инженеров, которые интегрируют SDK в приложение.

Gravity Field SDK

# Добавление библиотеки в проект

  1. Из корня проекта вызовите команду:
flutter pub add gravity_sdk
  1. После добавления плагина в файле pubspec.yaml появится строка с зависимостью:
dependencies:
  gravity_sdk: ^0.10.2 # Замените на актуальную версию
  1. Добавьте импорт:
import 'package:gravity_sdk/gravity_sdk.dart';

Требования к версиям:

  • Dart SDK: >=3.6.0
  • Flutter: >=1.17.0

Для iOS: Добавьте в файл ios/Runner/Info.plist ключ NSUserTrackingUsageDescription для запроса разрешения на отслеживание:

<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>

# Быстрый старт

Этот раздел проведет вас через основные шаги интеграции SDK, от инициализации до запуска вашей первой кампании.

# Шаг 1: Инициализация SDK

Сначала необходимо инициализировать SDK с вашим apiKey и section. Это лучше всего делать в функции main() вашего приложения. Критически важно подключить gravityEventCallback для обработки навигации и других действий.

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:gravity_sdk/gravity_sdk.dart';

void main() async {
  // Обязательно до runApp
  WidgetsFlutterBinding.ensureInitialized();

  // Инициализация SDK
  await GravitySDK.instance.initialize(
    apiKey: 'YOUR_API_KEY',
    section: 'YOUR_SECTION_ID',
    // Обработка действий
    gravityEventCallback: (event) {
      print('Gravity SDK Event: ${event.runtimeType}');

      // Обработка перехода по внешней ссылке
      if (event is FollowUrlEvent) {
        launchUrl(Uri.parse(event.url), mode: LaunchMode.externalApplication);
      }

      // Обработка перехода по диплинку
      if (event is FollowDeeplinkEvent) {
        // Здесь ваша логика навигации по диплинкам.
        // Например, с использованием GoRouter:
        // context.go(event.deeplink);
        print('Follow Deeplink: ${event.deeplink}');
      }
    },
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // ...
  }
}

# Почему важен gravityEventCallback?

SDK не выполняет навигацию самостоятельно. Когда пользователь нажимает на кнопку в кампании, SDK генерирует событие (например, FollowUrlEvent) и передает его в gravityEventCallback. Ваше приложение должно «поймать» это событие и выполнить соответствующее действие (открыть ссылку, перейти на другой экран). Без этого интерактивные элементы работать не будут.

Для открытия URL мы использовали пакет url_launcher. Не забудьте добавить его в ваш pubspec.yaml:

dependencies:
  url_launcher: ^6.3.1

Подробнее о событиях см. раздел Обработка обратных вызовов (Callbacks). Для аналитики действий из колбэка (клики по ссылкам/диплинкам, запросы разрешений и т.д.) используйте triggerEvent с CustomEvent.

# Шаг 2: Идентификация пользователя

Когда пользователь входит в систему, важно связать его действия, совершенные анонимно, с его постоянным профилем. Для этого после авторизации отправляется LoginEvent. Это рекомендуемый способ идентификации.

// Вызывается после успешной авторизации пользователя
void onUserLoggedIn(BuildContext context, String rawPhoneNumber) {
  // 1. Нормализуем номер телефона:
  // "+7 (999) 000-00-00" -> "79990000000"
  // "8 (999) 000-00-00"  -> "79990000000" (для РФ/КЗ приводим к формату 7XXXXXXXXXX)
  final normalizedPhone = normalizePhone(rawPhoneNumber);

  // 2. Строим SHA-256 хеш нормализованной строки (UTF-8, lowercase hex)
  final hashedPhone = sha256Hex(normalizedPhone);

  final loginEvent = LoginEvent(
    cuid: hashedPhone,      // SHA-256 хеш нормализованного телефона
    cuidType: 'phone_hash', // Стандартный тип идентификатора
  );

  GravitySDK.instance.triggerEvent(
    context: context,
    events: [loginEvent],
    pageContext: PageContext(
      type: ContextType.other,
      data: [],
      location: 'app://login',
    ),
  );
}

Отправка LoginEvent "склеивает" анонимный профиль с профилем авторизованного пользователя, сохраняя всю историю его действий.

Подробнее о различных способах идентификации читайте в разделе Идентификация пользователя.

# Шаг 3: Отслеживание просмотров экранов

Gravity Field принимает решение о показе персонализированного контента на основе контекста страницы, который передаёт SDK. Давайте отследим просмотр главной страницы.

// В коде виджета вашей главной страницы
void trackHomepageView(BuildContext context) {
  GravitySDK.instance.trackView(
    context: context,
    pageContext: PageContext(
      type: ContextType.homepage,
      data: [],
      location: 'app://homepage',
    ),
  );
}

Важно: если PageContext содержит значения из товарного фида, передавайте их без изменений относительно фида. Это относится к SKU, иерархии категорий, lng и другим feed-derived значениям. Регистр является частью значения: не приводите его к upper/lower case.

Если для этого события настроена in-app кампания, SDK автоматически покажет ее.

# Почему SDK требует BuildContext?

SDK использует BuildContext для доступа к дереву виджетов и ThemeData вашего приложения. Это позволяет:

  1. Найти ScaffoldMessenger для показа SnackBar.
  2. Использовать Navigator для отображения диалогов, шторок и полноэкранных кампаний.
  3. Наследовать стили (шрифты, цвета) из глобальной темы, чтобы кампании выглядели нативно.

# Шаг 4: Отслеживание событий

Теперь отследим добавление товара в корзину. Это событие может запустить кампанию с товарными рекомендациями (например, "с этим товаром покупают").

void trackAddToCart(BuildContext context, String productId) {
  final event = AddToCartEvent(
    value: 99.99,
    productId: productId,
    quantity: 1,
    currency: 'RUB',
  );

  GravitySDK.instance.triggerEvent(
    context: context,
    events: [event],
    pageContext: PageContext(
      type: ContextType.product,
      data: [productId],
      location: 'app://product/$productId',
    ),
  );
}

Важно: если в событии или PageContext передаются значения, которые должны матчиться с товарным фидом, используйте их в точности как в фиде. Это относится к productId, cart[*].productId, SKU в data, категориям, lng и другим feed-derived значениям. Регистр должен совпадать с фидом.

# Шаг 5: Отображение inline-кампаний

Для отображения рекомендаций прямо в верстке страницы (например, блок "Персональные рекомендации") используйте виджет GravityInlineWidget.

// В методе build вашего виджета
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Inline Recommendations')),
    body: ListView(
      children: [
        // ... другие виджеты
        GravityInlineWidget(
          selector: 'homepage-recommendations',
          height: 250, // Обязательно задайте высоту для блока
          pageContext: PageContext(
            type: ContextType.homepage,
            data: [],
            location: 'app://homepage',
          ),
        ),
        // ... другие виджеты
      ],
    ),
  );
}

Виджет сам загрузит и отобразит релевантный контент по селектору homepage-recommendations.

# Инициализация и конфигурация

# initialize()

Основной метод для настройки SDK. Вызывается один раз при старте приложения.

Future<void> initialize({
  required String apiKey,
  required String section,
  ProductWidgetBuilder? productWidgetBuilder,
  GravityEventCallback? gravityEventCallback,
  GravityContentCallback? gravityContentCallback,
  LogLevel logLevel = LogLevel.info,
});
  • apiKey (required): Ваш уникальный ключ API.
  • section (required): Идентификатор секции вашего проекта.
  • productWidgetBuilder: Опциональный билдер для кастомизации карточек товаров. См. раздел Работа с контентом.
  • gravityEventCallback: Опциональный колбэк для событий SDK и действий пользователя внутри кампаний. См. раздел Обработка обратных вызовов (Callbacks).
  • gravityContentCallback: Опциональный колбэк для headless-сценариев, где нужно получить GravityDataResponse<ContentResponse> вместе с исходным JSON.
  • logLevel: Уровень детализации логов SDK. По умолчанию LogLevel.info. См. раздел Настройка логирования.

# setOptions()

Позволяет задать глобальные настройки для всех последующих запросов.

void setOptions({
  Options? options,
  ContentSettings? contentSettings,
  String? proxyUrl,
  bool? isFetchContentOnTrack,
});
  • options: Настройки для управления поведением запросов.
  • contentSettings: Настройки для управления получаемым контентом.
  • proxyUrl: URL прокси-сервера для отправки запросов.
  • isFetchContentOnTrack: Включает или отключает автоматическую догрузку контента после trackView(...) и triggerEvent(...). По умолчанию true.

Пример:

GravitySDK.instance.setOptions(
  options: Options(
    isReturnUserInfo: true, // Возвращать информацию о пользователе в ответах
    isImplicitImpression: true, // Автоматически отправлять событие показа
  ),
  contentSettings: ContentSettings(
    skusOnly: false, // Возвращать полную информацию о продуктах, а не только SKU
    fields: ['name', 'price', 'imageUrl'], // Запросить конкретные поля
  ),
);

# Настройка логирования

При инициализации SDK вы можете указать уровень детализации логов с помощью параметра logLevel. Это полезно для отладки интеграции.

await GravitySDK.instance.initialize(
  apiKey: 'YOUR_API_KEY',
  section: 'YOUR_SECTION_ID',
  logLevel: LogLevel.debug, // Устанавливаем максимальный уровень детализации
);

Доступные уровни LogLevel:

  • LogLevel.none: Логирование полностью отключено.
  • LogLevel.error: Только ошибки.
  • LogLevel.warn: Ошибки и предупреждения.
  • LogLevel.info: (По умолчанию) Информационные сообщения, ошибки и предупреждения.
  • LogLevel.debug: Максимальная детализация, включая тела запросов и ответов.

# Идентификация пользователя

SDK поддерживает два подхода к идентификации: автоматический (управляется SDK) и ручной (управляется вашим приложением).

# Стандарт CUID на основе телефона (phone_hash)

Во всех интеграциях рекомендуется использовать единый идентификатор пользователя — SHA-256 хеш нормализованного мобильного телефона:

  • Перед хешированием номер очищается от всех символов, кроме цифр.
  • Для РФ/КЗ номер приводится к формату 7XXXXXXXXXX (без +, пробелов, скобок и дефисов).
  • Для других стран используется международный формат без + и разделителей.
  • Хеш считается по строке в кодировке UTF-8, результат — lowercase hex.
  • В LoginEvent.cuidType всегда указывается строка 'phone_hash'.

Тот же хеш и тип должны использоваться:

  • в Web-интеграции (JavaScript-событие Login),
  • в Server-Side API (/ssapi/event),
  • при импорте офлайн-данных.
  • Если в CDP или DWH уже рассчитывается этот идентификатор по тому же алгоритму, во всех типах интеграции (включая Flutter SDK) необходимо передавать именно тот хеш, который используется в CDP, чтобы обеспечить единый идентификатор ключ пользователя.

# Автоматическая идентификация (SDK-managed)

Это подход по умолчанию. При первом запросе SDK получает от сервера уникальный uid (user ID) и ses (session ID) и сохраняет их на устройстве. Все последующие запросы будут использовать эти идентификаторы.

Чтобы связать анонимный профиль с профилем авторизованного пользователя, после входа в систему отправьте LoginEvent.

// Вызывается после успешной авторизации
void onUserLoggedIn(BuildContext context, String rawPhoneNumber) {
  final normalizedPhone = normalizePhone(rawPhoneNumber);
  final hashedPhone = sha256Hex(normalizedPhone);

  final loginEvent = LoginEvent(
    cuid: hashedPhone,
    cuidType: 'phone_hash', // Стандартный тип идентификатора: SHA-256 хеш телефона
  );

  GravitySDK.instance.triggerEvent(
    context: context,
    events: [loginEvent],
    pageContext: PageContext(
      type: ContextType.other,
      data: [],
      location: 'app://login',
    ),
  );
}

# Ручная идентификация

Если ваше приложение уже управляет ID пользователей и сессий, вы можете передавать их в SDK напрямую с помощью метода setUser.

# setUser()

Устанавливает custom (ваш ID пользователя) и ses (ваш ID сессии) для всех последующих запросов.

void setUser(String userId, String sessionId);

Пример:

// Вызывается при старте сессии, если ID уже известны
GravitySDK.instance.setUser('user-from-my-system-42', 'session-from-my-system-xyz');

В этом режиме SDK не будет использовать автоматически сгенерированный uid.

# PageContext

PageContext описывает, где находится пользователь в приложении и в каком бизнес-контексте выполняется запрос.

Этот объект используется в ключевых вызовах Flutter SDK:

  • trackView(...) для передачи просмотров экранов;
  • triggerEvent(...) для передачи пользовательских действий;
  • getContentBySelector(...) для ручного получения контента;
  • getContentByGroup(...) для загрузки группы inline-кампаний.

Корректное заполнение PageContext влияет и на аналитику, и на подбор контента, и на активацию кампаний.

# Структура PageContext

class PageContext {
  final ContextType type;
  final List<String> data;
  final String location;
  final String? lng;
  final int? pageNumber;
  final String? referrer;
  final Map<String, String>? utm;
  final Map<String, Object> attributes;
}
Поле Обязательность Описание
type Обязательно Тип экрана.
data Обязательно Контекстные данные для выбранного типа экрана.
location Обязательно Уникальный идентификатор экрана, маршрута или deeplink.
lng Опционально Региональный код для мультирегиональности.
pageNumber Опционально Номер страницы в пагинации.
referrer Опционально Источник перехода.
utm Опционально UTM-метки.
attributes Опционально Дополнительные атрибуты таргетинга.

Бизнес-логика заполнения PageContext соответствует Page context.

Рекомендуемая схема:

  • HOMEPAGE: data = [].
  • PRODUCT: в data передается SKU товара ровно как в товарном фиде, без нормализации и с тем же регистром.
  • CART: в data передаются SKU всех товаров, находящихся в корзине, ровно как в товарном фиде, без нормализации и с тем же регистром.
  • CATEGORY: в data передается полная иерархия категорий от самой широкой до самой узкой ровно как в товарном фиде, без нормализации и с тем же регистром.
  • SEARCH: в data передается поисковый запрос одной строкой. Для пустого поиска передается пустой список.
  • OTHER: используется только для экранов, которые не подходят под остальные типы.

Для любых значений в PageContext, которые должны матчиться с товарным фидом, действует единое правило: передавайте их идентично фиду. Нельзя менять написание, переименовывать значения или приводить их к upper/lower case.

lng используется для мультирегиональности. Это позволяет отдавать корректные региональные данные по товарам: цены, доступность и остатки.

Например, если пользователь находится в Новосибирске, значение lng можно использовать для того, чтобы в рекомендациях участвовали только товары, реально доступные в Новосибирске.

Важно:

  • значения lng в контексте и в товарном фиде должны совпадать полностью, включая регистр;
  • lng, как и любые другие feed-derived значения, нужно передавать без преобразования к upper/lower case;
  • lng нужно передавать только если проект использует региональные варианты данных в фиде.

SDK автоматически дополняет attributes служебными значениями app_version, sdk_version и app_platform.

См. также:

  • trackView(...)
  • triggerEvent(...)
  • getContentBySelector(...)
  • getContentByGroup(...)

# Отслеживание просмотров экранов

# trackView(...)

trackView(...) передает в Gravity Field факт просмотра экрана. Этот вызов используется и для трекинга пользовательского поведения, и для сценариев, в которых показ кампании зависит от контекста страницы.

Future<void> trackView({
  required BuildContext context,
  required PageContext pageContext,
});
  • context: BuildContext текущего экрана.
  • pageContext: корректно заполненный PageContext.

Пример:

GravitySDK.instance.trackView(
  context: context,
  pageContext: PageContext(
    type: ContextType.product,
    data: ['product-sku-123'],
    location: 'app://product/123',
  ),
);

Если для переданного контекста настроена in-app кампания, SDK может загрузить и показать ее автоматически.

# Почему SDK требует BuildContext?

Flutter SDK использует BuildContext для показа SnackBar, модальных окон, bottom sheet и полноэкранных кампаний, а также для доступа к навигации и теме приложения.

# Трекинг событий

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

Бизнес-логика заполнения событий соответствует Настройке передачи событий, а Flutter SDK предоставляет типы и методы для их отправки.

Для e-commerce сценариев события PurchaseEvent и AddToCartEvent обязательны. Остальные события внедряются по релевантности вашему сценарию.

Для всех полей событий, которые должны матчиться с товарным фидом, действует то же правило, что и для PageContext: передавайте значения идентично фиду. Это относится к productId, cart[*].productId и другим feed-derived значениям. Регистр является частью значения.

# triggerEvent(...)

triggerEvent(...) передает в Gravity Field пользовательские действия. Этот вызов нужен и для аналитики, и для сегментации, и для сценариев, в которых событие может активировать кампанию.

Future<void> triggerEvent({
  required BuildContext context,
  required List<TriggerEvent> events,
  required PageContext pageContext,
});
  • context: BuildContext текущего экрана.
  • events: список событий для отправки.
  • pageContext: корректно заполненный PageContext.

Пример:

GravitySDK.instance.triggerEvent(
  context: context,
  events: [
    AddToCartEvent(
      value: 1500.0,
      productId: 'sku-abc-1',
      quantity: 1,
      currency: 'RUB',
    ),
  ],
  pageContext: PageContext(
    type: ContextType.product,
    data: ['sku-abc-1'],
    location: 'app://product/sku-abc-1',
  ),
);

# Покупка (PurchaseEvent)

Отправляется после успешного завершения заказа.

final purchaseEvent = PurchaseEvent(
  uniqueTransactionId: 'ORDER-12345',
  value: 2550.75,
  currency: 'RUB',
  cart: [
    CartItem(productId: 'sku-123', quantity: 1, itemPrice: 100.50),
    CartItem(productId: 'sku-456', quantity: 2, itemPrice: 1225.125),
  ],
);

Правила заполнения:

  • uniqueTransactionId должен быть уникальным для каждой покупки;
  • value — полная сумма заказа;
  • currency опциональна, но обязательна для мультивалютных проектов;
  • cart содержит фактический состав заказа;
  • каждый cart[*].productId должен совпадать со SKU в товарном фиде полностью, включая регистр;
  • товары в cart рекомендуется передавать в порядке добавления: от самых старых к самым новым;
  • itemPrice — стоимость одной единицы товара после применения скидок.

# Добавление в корзину (AddToCartEvent)

Отправляйте событие в момент фактического добавления товара в корзину.

final addToCartEvent = AddToCartEvent(
  value: 1500.0,
  productId: 'sku-abc-1',
  quantity: 1,
  currency: 'RUB',
);

Правила заполнения:

  • value — сумма, добавляемая в корзину этим действием;
  • quantity — количество единиц, добавленных именно этим действием;
  • productId должен совпадать со SKU в товарном фиде полностью, включая регистр;
  • currency опциональна, но обязательна для мультивалютных проектов;
  • cart, если передается, должен содержать актуальное состояние корзины, включая только что добавленный товар. Все cart[*].productId должны совпадать со SKU в товарном фиде полностью, включая регистр.

# Удаление из корзины (RemoveFromCartEvent) и синхронизация корзины (SyncCartEvent)

  • RemoveFromCartEvent отправляйте в момент удаления товара из корзины или уменьшения количества;
  • value в RemoveFromCartEvent — сумма удаляемых единиц товара;
  • quantity в RemoveFromCartEvent — количество единиц, удаленных этим действием;
  • SyncCartEvent используйте, когда нужно передать актуальное состояние корзины целиком;
  • value в SyncCartEvent — общая стоимость актуального состава корзины;
  • productId в RemoveFromCartEvent должен совпадать со SKU в товарном фиде полностью, включая регистр;
  • cart в SyncCartEvent должен содержать полное текущее состояние корзины. Все cart[*].productId должны совпадать со SKU в товарном фиде полностью, включая регистр.

# Вход в систему (LoginEvent)

final loginEvent = LoginEvent(
  cuid: sha256Hex(normalizePhone(rawPhoneNumber)),
  cuidType: 'phone_hash',
);

Поле hashedEmail опционально. Если email не является обязательным в вашем процессе регистрации или логина, его можно не передавать.

# Добавление в избранное (AddToWishlistEvent)

final addToWishlistEvent = AddToWishlistEvent(
  value: 1500.0,
  productId: 'sku-abc-1',
);

productId в AddToWishlistEvent должен совпадать со SKU в товарном фиде полностью, включая регистр.

# Кастомное событие (CustomEvent)

final customEvent = CustomEvent(
  type: 'survey-completed-v1',
  name: 'Survey completed',
  customProps: {
    'surveyId': 'summer-2025-feedback',
    'rating': '5',
  },
);

# Передача статуса Push-уведомлений

Если таргетинг кампаний зависит от статуса push-разрешения, передавайте его в SDK.

# setNotificationPermissionStatus()

void setNotificationPermissionStatus(NotificationPermissionStatus status);

Допустимые значения:

  • NotificationPermissionStatus.granted
  • NotificationPermissionStatus.denied
  • NotificationPermissionStatus.unknown

Пример:

import 'package:firebase_messaging/firebase_messaging.dart';

Future<NotificationPermissionStatus> getNotificationStatus() async {
  final settings = await FirebaseMessaging.instance.getNotificationSettings();

  switch (settings.authorizationStatus) {
    case AuthorizationStatus.authorized:
    case AuthorizationStatus.provisional:
      return NotificationPermissionStatus.granted;
    case AuthorizationStatus.denied:
      return NotificationPermissionStatus.denied;
    default:
      return NotificationPermissionStatus.unknown;
  }
}

final status = await getNotificationStatus();
GravitySDK.instance.setNotificationPermissionStatus(status);

# Работа с контентом

Flutter SDK поддерживает три основных режима работы с контентом:

  • in-app кампании, которые SDK показывает самостоятельно;
  • inline кампании, которые встраиваются в экран приложения;
  • JSON/manual rendering, когда приложение само интерпретирует ответ и строит UI.

# In-App кампании

SDK автоматически отображает in-app кампании, если они приходят в ответ на trackView(...) или triggerEvent(...).

Приложение отвечает за:

  • корректный PageContext;
  • корректный BuildContext;
  • обработку действий пользователя через gravityEventCallback.

# GravityInlineWidget

Используйте GravityInlineWidget для загрузки и отображения одного inline-блока по selector.

GravityInlineWidget(
  selector: 'homepage-recs',
  height: 250,
  pageContext: PageContext(
    type: ContextType.homepage,
    data: [],
    location: 'app://homepage',
  ),
)

Для мультивиджет-кампаний используйте placeholderId.

# Параметры виджета

Параметр Тип Описание
selector String Селектор кампании.
placeholderId String? Идентификатор placeholder внутри мультивиджет-кампании.
width double? Ширина виджета.
height double? Высота виджета.
pageContext PageContext Контекст экрана.
showLoading bool Показывать ли состояние загрузки. По умолчанию true.
loadingWidget Widget? Кастомный виджет для состояния загрузки.

# GravityInlineListWidget

GravityInlineListWidget используется для отображения нескольких inline-элементов из одной группы.

GravityInlineListWidget(
  group: 'homepage-group',
  height: 250,
  pageContext: PageContext(
    type: ContextType.homepage,
    data: [],
    location: 'app://homepage',
  ),
)

Этот виджет использует публичный метод getContentByGroup(...).

# ProductWidgetBuilder

Если нужно полностью контролировать отображение карточек товара, реализуйте ProductWidgetBuilder.

Важно:

  • SDK автоматически отслеживает показ товара;
  • клик по товару в кастомной карточке нужно отправлять вручную через sendProductEngagement(...).
class CustomProductWidgetBuilder extends ProductWidgetBuilder {
  @override
  Widget build({
    required BuildContext context,
    required Slot product,
    required CampaignContent content,
    required Campaign campaign,
  }) {
    return GestureDetector(
      onTap: () {
        GravitySDK.instance.sendProductEngagement(
          ProductClickEngagement(product, content, campaign),
        );
      },
      child: Card(
        child: Text(product.item['name'] as String? ?? ''),
      ),
    );
  }
}

# JSON и manual rendering

Используйте JSON-режим, если приложение должно само интерпретировать ответ и строить UI без встроенных компонентов SDK.

# getContentBySelector(...)

Future<ContentResponse> getContentBySelector({
  required String selector,
  required PageContext pageContext,
});

Для getContentBySelector(...) требуется корректно заполненный PageContext.

Пример:

final response = await GravitySDK.instance.getContentBySelector(
  selector: 'homepage-inline-banner',
  pageContext: PageContext(
    type: ContextType.homepage,
    data: [],
    location: 'app://home',
  ),
);

if (response.data.isNotEmpty) {
  final campaign = response.data.first;
  final variation = campaign.payload.firstOrNull;
  final content = variation?.contents.firstOrNull;
  // Отрисуйте content в своем UI
}

# getContentByGroup(...)

Future<ContentResponse> getContentByGroup({
  required String group,
  required PageContext pageContext,
});

Этот метод используется для ручного получения группы кампаний и лежит в основе GravityInlineListWidget.

# Headless API

Flutter SDK также предоставляет дополнительные публичные методы для headless-сценариев:

  • getContentBySelectorWithDetails(...)
  • getContentByCampaignIdWithDetails(...)
  • trackViewNoShow(...)
  • triggerEventNoShow(...)
  • gravityContentCallback

Используйте их, если нужно получить GravityDataResponse<ContentResponse> вместе с исходным JSON или получить контент без автоматического показа UI.

# Трекинг взаимодействий (engagement)

Engagement-события позволяют фиксировать показы и клики по контенту и товарам.

# Какие события SDK отправляет автоматически

Для встроенных UI-компонентов SDK сам отправляет базовые события показа.

Для кастомного UI вручную нужно отправлять только те взаимодействия, которые SDK не может определить сам.

# sendContentEngagement()

void sendContentEngagement(ContentEngagement engagement)

Поддерживаемые типы:

  • ContentImpressionEngagement(content, campaign)
  • ContentVisibleImpressionEngagement(content, campaign)
  • ContentCloseEngagement(content, campaign)

# sendProductEngagement()

void sendProductEngagement(ProductEngagement engagement)

Поддерживаемые типы:

  • ProductClickEngagement(slot, content, campaign)
  • ProductVisibleImpressionEngagement(slot, content, campaign)

# Когда нужно отправлять engagement вручную

  • при полностью ручном JSON-рендеринге;
  • при кастомном рендеринге карточек товара через ProductWidgetBuilder;
  • в других сценариях, где вы сами контролируете жизненный цикл видимости и клика.

# Откуда брать slot, content и campaign

Чаще всего эти объекты берутся из ответа getContentBySelector(...).

final response = await GravitySDK.instance.getContentBySelector(
  selector: 'homepage-inline-banner',
  pageContext: pageContext,
);

final campaign = response.data.firstOrNull;
final variation = campaign?.payload.firstOrNull;
final content = variation?.contents.firstOrNull;
final slot = content?.products?.slots?.firstOrNull;

После этого можно отправлять engagement:

if (content != null && campaign != null) {
  GravitySDK.instance.sendContentEngagement(
    ContentVisibleImpressionEngagement(content, campaign),
  );
}

if (slot != null && content != null && campaign != null) {
  GravitySDK.instance.sendProductEngagement(
    ProductClickEngagement(slot, content, campaign),
  );
}

Если вы используете ProductWidgetBuilder, SDK сам передает вам slot, content и campaign в метод build(...).

# Справочник по событиям (TriggerEvent)

Событие Описание Ключевые параметры
AddToCartEvent Добавление товара в корзину value, productId, quantity, currency?, cart?
PurchaseEvent Успешная покупка uniqueTransactionId, value, cart, currency?
RemoveFromCartEvent Удаление товара из корзины value, productId, quantity, currency?, cart?
SyncCartEvent Передача актуального состава корзины value, currency?, cart?
AddToWishlistEvent Добавление в избранное value, productId
SignUpEvent Регистрация пользователя hashedEmail?, cuid?, cuidType?
LoginEvent Авторизация пользователя hashedEmail?, cuid?, cuidType?
CustomEvent Кастомное событие type, name, customProps?

# Обработка обратных вызовов (Callbacks)

Подпишитесь на события SDK, передав функцию gravityEventCallback в initialize.

# gravityEventCallback

gravityEventCallback получает события SDK, связанные с загрузкой контента, показами и действиями пользователя внутри кампаний.

# gravityContentCallback

gravityContentCallback предназначен для headless-сценариев и получает GravityDataResponse<ContentResponse> вместе с исходным JSON.

# Справочник по событиям (TrackingEvent)

Событие Описание Требует обработки?
ContentLoadEvent Контент кампании загружен. Нет
ContentImpressionEvent Контент кампании показан. Нет
ContentVisibleImpressionEvent Контент стал видимым на экране. Нет
ContentCloseEvent Пользователь закрыл in-app. Нет
CopyEvent Пользователь скопировал данные. Нет
CancelEvent Пользователь отменил действие. Нет
FollowUrlEvent Пользователь нажал на внешнюю ссылку. Да
FollowDeeplinkEvent Пользователь нажал на внутреннюю ссылку. Да
RequestPushEvent Пользователь нажал кнопку запроса push-разрешений. Да
ProductImpressionEvent Карточка товара стала видимой. Нет

Пример обработки:

gravityEventCallback: (event) {
  if (event is FollowUrlEvent) {
    // Открыть URL с помощью url_launcher
  } else if (event is FollowDeeplinkEvent) {
    // Выполнить навигацию с помощью вашего роутера
  } else if (event is RequestPushEvent) {
    // Запросить разрешение на push-уведомления
  }
}

RequestPushEvent — это сигнал приложению запросить push-permission. Flutter SDK сам системный диалог не открывает.

# FAQ / Troubleshooting

Q: В чем разница между trackView и triggerEvent?
A: Оба метода могут инициировать показ кампании, но их роль шире. trackView(...) передает факт просмотра экрана и используется для аналитики поведения на экранах. triggerEvent(...) передает конкретные действия пользователя и используется для аналитики, сегментации и сценариев, активируемых событиями.

Q: Почему GravityInlineWidget не отображается или имеет нулевую высоту?
A: Самая частая причина — для GravityInlineWidget не задан параметр height.

Q: Почему клик по кнопке в кампании не приводит к переходу?
A: Не обработаны FollowUrlEvent или FollowDeeplinkEvent в gravityEventCallback. SDK не выполняет навигацию сам.

Q: Что произойдет, если SDK не сможет получить кампанию от сервера?
A: SDK обработает ошибку внутри. Inline-компонент просто не будет отображен, а in-app кампания не будет показана.

Q: Почему в ProductWidgetBuilder нужно самому отправлять клик?
A: Потому что вы управляете кастомным UI. SDK не знает, какой именно элемент внутри вашей карточки следует считать кликом по товару.