# 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.9.1 # Замените на актуальную версию
  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, чтобы видеть события SDK в консоли.

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) {
    // ...
  }
}

Чтобы интерактивные элементы в ваших In-App кампаниях (например, кнопки "Купить" или "Подробнее") работали, необходимо обрабатывать события, которые SDK отправляет в gravityEventCallback. Без этой обработки клики по ссылкам и диплинкам не приведут к навигации.

В примере инициализации показано, как обрабатывать FollowUrlEvent для открытия внешних ссылок и FollowDeeplinkEvent для внутренней навигации в приложении. Убедитесь, что вы реализовали эту логику.

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

Подробнее о всех возможных событиях читайте в разделе Обработка обратных вызовов (Callbacks).

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

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

// Вызывается после успешной авторизации пользователя
void onUserLoggedIn(BuildContext context, String userCustomId) {
  final loginEvent = LoginEvent(cuid: userCustomId, cuidType: 'mySystemUserId');

  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',
    ),
  );
}

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

# Шаг 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',
    ),
  );
}

# Шаг 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, // Задайте высоту для блока
        ),
        // ... другие виджеты
      ],
    ),
  );
}

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

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

# initialize()

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

Future<void> initialize({
  required String apiKey,
  required String section,
  ProductWidgetBuilder? productWidgetBuilder,
  GravityEventCallback? gravityEventCallback,
});
  • apiKey (required): Ваш уникальный ключ API.
  • section (required): Идентификатор секции вашего проекта.
  • productWidgetBuilder: Опциональный билдер для кастомизации виджетов продуктов. (См. раздел 8. Кастомизация UI).
  • gravityEventCallback: Опциональный колбэк для получения уведомлений о событиях SDK. (См. раздел Обработка обратных вызовов (Callbacks)).

# setOptions()

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

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

Пример:

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

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

Идентификация пользователя — фундамент для персонализации, аналитики и построения омниканальных профилей в Gravity Field.

Без корректной идентификации невозможно:

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

# Варианты идентификации

Gravity Field позволяет автоматически или вручную управлять идентификацией, гибко подстраиваясь под архитектуру вашего проекта.

Gravity Field UID (автоматическая идентификация)

Когда использовать:

  • Когда нет собственного механизма учёта пользователей и сессий

Как работает:

  • При первом запросе к Gravity Field (например, trackView(...)), если нет сохранённых идентификаторов, Gravity Field автоматически создаёт уникальные идентификаторы:
    • uid — уникальный идентификатор пользователя, автоматически сгенерированный Gravity Field
    • ses — ID сессии, автоматически сгенерированный Gravity Field
  • Эти значения возвращаются в ответе и сохраняются.
  • SDK будет использовать их во всех следующих запросах.
  • Сессия живёт 30 минут и продлевается при активности пользователя.

После авторизации:

  • Чтобы связать анонимный профиль с авторизованным, вызовите событие login через triggerEvent(...).
  • Gravity Field объединит поведение до и после логина.

Пример отправки LoginEvent:

// Вызывается после успешной авторизации
void onUserLoggedIn(BuildContext context, String userCustomId) {
  final loginEvent = LoginEvent(
    cuid: userCustomId,
    cuidType: 'mySystemUserId' // Укажите тип вашего идентификатора
  );

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

Custom user id (идентификация через ID приложения)

Когда использовать:

  • Если у пользователя в приложении есть собственный ID и логика управления сессиями.

Как работает:

  • Разработчик вызывает setUser(...).
    • userId — ваш собственный ID пользователя (custom ID);
    • sessionId — ID текущей сессии.
  • SDK использует эти значения в каждом запросе.

💡 Если вы передаёте userId, вы обязаны также передать sessionId. SDK не может создать sessionId автоматически в этом режиме.

# setUser(...)

Позволяет явно установить параметры пользователя и сессии, которые будут автоматически включаться во все запросы SDK к API (/visit, /event, /choose, /engagement).

void setUser(String customId, String sessionId);

Пример:

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

# Как SDK работает с идентификаторами

SDK автоматически управляет идентификаторами пользователя и сессии, опираясь на то, что уже сохранено локально. Используется следующая логика:

  1. Если вызван setUser(...) — SDK использует переданные userId и sessionId (режим: custom user ID).
  2. Если setUser(...) не вызывался, но уже есть uid и ses — SDK использует их (режим: Gravity Field UID).
  3. Если ни один ID не сохранён — SDK отправляет запрос без идентификаторов. Gravity Field создаёт uid и ses, SDK сохраняет их и использует в дальнейшем.

# Передача контекста

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

Контекст страницы позволяет Gravity Field понять:

  • Где сейчас находится пользователь в приложении
  • Какие товары или категории он просматривает
  • В каком регионе или в каких условиях пользователь находится

# trackView(...)

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

Future<void> trackView({
  required BuildContext context,
  required PageContext pageContext,
});
  • context: BuildContext текущего экрана.
  • pageContext: Контекст страницы, который описывает, где находится пользователь.

Пример:

// На экране продукта
GravitySDK.instance.trackView(
  context: context,
  pageContext: PageContext(
    type: ContextType.product,
    data: ['product-sku-123'], // SKU продукта
    location: 'app://product/123',
  ),
);

# PageContext

PageContext — ключевая модель для описания местоположения и контекста пользователя в приложении.

class PageContext {
  final ContextType type;
  final List<String> data;
  final String location;
  final String? lng;
  final Map<String, String>? utm;
  final Map<String, Object>? attributes;
}
  • type: Тип страницы (ContextType).
  • data: Массив строк с данными, зависящими от типа.
  • location: Уникальный идентификатор местоположения (URL, deeplink, название экрана).
  • lng: Язык.
  • utm: UTM-метки.
  • attributes: Дополнительные атрибуты.

Примеры PageContext для разных ContextType:

  • homepage: PageContext(type: ContextType.homepage, data: [], location: 'app://home')
  • product: PageContext(type: ContextType.product, data: ['sku-123'], location: 'app://product/123')
  • cart: PageContext(type: ContextType.cart, data: ['sku-123', 'sku-456'], location: 'app://cart')
  • category: PageContext(type: ContextType.category, data: ['Электроника'], location: 'app://category/electronics')
  • search: PageContext(type: ContextType.search, data: ['запрос'], location: 'app://search?q=запрос')
  • other: PageContext(type: ContextType.other, data: [], location: 'app://about')

💡 Типы контекста и соответсвующие для них data описаны здесь: Page context

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

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

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

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

Платформа поддерживает как системные (предопределённые) события, так и произвольные пользовательские события.

  • Системные события — это события с зафиксированными type, которые платформа распознаёт и может использовать для активации кампаний или анализа (например, purchase, add_to_cart, login).
  • Пользовательские события — разработчик может отправлять любые события с кастомным type. Такие события будут использоваться для аналитики и построения собственных отчётов или логики в Gravity Field.

# triggerEvent(...)

Отправляет одно или несколько событий. В ответ, как и в случае с trackView, может прийти кампания для показа.

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

Ниже приведены примеры кода для отслеживания наиболее распространенных бизнес-событий с помощью triggerEvent().

# Покупка (PurchaseEvent)

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

final purchaseEvent = PurchaseEvent(
  uniqueTransactionId: 'ORDER-12345', // Уникальный ID транзакции
  value: 2550.75,
  currency: 'RUB',
  cart: [
    CartItem(productId: 'sku-123', quantity: 1, itemPrice: 100.50),
    CartItem(productId: 'sku-456', quantity: 1, itemPrice: 50.00),
    CartItem(productId: 'sku-abc-1', quantity: 1, itemPrice: 1500.00),
    CartItem(productId: 'sku-def-2', quantity: 2, itemPrice: 525.375),
  ],
);

GravitySDK.instance.triggerEvent(
  context: context,
  events: [purchaseEvent],
  pageContext: PageContext(
    type: ContextType.other,
    data: [],
    location: 'app://checkout/success',
  ),
);

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

Отправляется, когда пользователь добавляет товар в корзину.

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

GravitySDK.instance.triggerEvent(
  context: context,
  events: [addToCartEvent],
  pageContext: PageContext(
    type: ContextType.product,
    data: ['sku-abc-1'],
    location: 'app://product/sku-abc-1',
  ),
);

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

Отправляется после успешной аутентификации пользователя.

final loginEvent = LoginEvent(
  // Передайте один из идентификаторов
  cuid: 'customer-12345',
  cuidType: 'mySystemUserId',
  // hashedEmail: '...' // или хешированный email
);

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

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

Для отслеживания любых других действий, не покрытых стандартными событиями.

final customEvent = CustomEvent(
  type: 'survey-completed-v1', // Уникальный тип события
  name: 'Опрос пройден', // Человекочитаемое имя
  properties: {
    'surveyId': 'summer-2025-feedback',
    'rating': '5',
  },
);

GravitySDK.instance.triggerEvent(
  context: context,
  events: [customEvent],
  pageContext: PageContext(
    type: ContextType.other,
    data: [],
    location: 'app://survey/summer-2025-feedback',
  ),
);

Подробное описание всех доступных событий и их параметров смотрите в разделе Справочник по событиям (TriggerEvent).

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

# Inline-блоки (встраиваемые кампании в UI)

Кампания встраивается в существующую структуру экрана приложения — но только если условия показа, заданные на платформе Gravity Field, выполнены.

GravityInlineWidget

SDK запрашивает кампанию по selector, передаёт контекст текущей страницы, и если сервер вернёт релевантный контент — он будет отображён в заданном UI-элементе.

GravityInlineWidget({
  required String selector,
  String? placeholderId,
  double? width,
  double? height,
  Map<String, dynamic>? attributes,
})
Параметр Тип Описание
selector String Уникальный идентификатор кампании, используемый для запроса контента
width double? Ширина блока. По умолчанию занимает всё доступное пространство?
height double? Высота блока. Может быть зафиксирована вручную.
attributes Map<String, dynamic>? Дополнительные параметры, которые передаются вместе с запросом кампании
placeholderId String ID места в интерфейсе, куда вставляется виджет, в мультивиджет кампании

selector задается в настройках кампании, в личном кабинете Gravity Field.

placeholderId задается в настройках вариации кампании, в лично кабинете Gravity Field

💡 Если условия показа не выполнены (например, пользователь не входит в целевую группу), блок не будет отрисован. ⚠️ Несмотря на то что параметр height является опциональным, его необходимо передавать, иначе виджет может "схлопнуться" до нулевой высоты и не будет виден.

📌 Подходит для: баннеров, товарных рекомендаций, УТП блоков.

Пример:

GravityInlineWidget(
  selector: 'homepage-recommendations',
  // Опционально: задайте размеры
  height: 250,
  // Опционально: передайте контекст, если он отличается от общего
  pageContext: PageContext(type: ContextType.homepage, data: [], location: 'app://home'),
)

# Кастомизация внешнего вида товарных рекомендаций

Чтобы карточки товаров в кампаниях выглядели в стиле вашего приложения, создайте свой класс, наследующий ProductWidgetBuilder, и передайте его экземпляр в initialize.

// 1. Создайте свой билдер
class CustomProductWidgetBuilder extends ProductWidgetBuilder {
  @override
  Widget build({
    required BuildContext context,
    required Slot product,
    required CampaignContent content,
    required Campaign campaign,
  }) {
    // Ваша кастомная верстка карточки товара
    return Card(
      child: ListTile(
        leading: Image.network(product.item.imageUrl ?? ''),
        title: Text(product.item.name),
        subtitle: Text('${product.item.price} RUB'),
        onTap: () {
          // При клике важно отправлять событие взаимодействия
          GravitySDK.instance.sendProductEngagement(
            ProductClickEngagement(product, content, campaign)
          );
          // ... и выполнять переход на экран товара
        },
      ),
    );
  }
}

// 2. Передайте его в initialize
await GravitySDK.instance.initialize(
  apiKey: 'YOUR_API_KEY',
  section: 'YOUR_SECTION_ID',
  productWidgetBuilder: CustomProductWidgetBuilder(),
);

# Отображение JSON-кампаний (ручной рендеринг)

В некоторых случаях кампания может возвращать не готовый UI, а чистые данные в формате JSON. Это дает вам полный контроль над тем, как этот контент будет выглядеть и вести себя в вашем приложении. Вы несете ответственность за рендеринг UI на основе полученных данных.

# getContentBySelector(...)

Основной способ получить такие данные — использовать метод getContentBySelector.

Пример:

// Получаем контент для инлайнового блока на главной
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;
  // ... ваша логика отображения
}

# Событийная модель

Вы также можете получать JSON-кампании, которые активируются в ответ на события (trackView, triggerEvent). Для этого подпишитесь на gravityEventCallback и отлавливайте событие ContentLoadEvent. Этот подход полезен, когда кампания должна появиться не по прямому запросу, а как реакция на действие пользователя.

await GravitySDK.instance.initialize(
  // ... другие параметры
  gravityEventCallback: (event) {
    if (event is ContentLoadEvent) {
      // Проверяем, что это нужная нам кампания
      if (event.type == 'LoadEvent' && event.selector == 'custom_popup') {
        // Здесь ваша логика для обработки и отображения данных из event.content
        print('Получена JSON-кампания: ${event.content.contentId}');
      }
    }
  },
);

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

Engagement-события для кампаний позволяют Gravity Field анализировать поведение пользователя в деталях:

  • Какой контент был показан и как он повлиял на поведение
  • Какая вариация кампании эффективнее: какой баннер чаще просматривают, на какой товар чаще кликают, что влияет на покупку
  • Понимание того, какие элементы UI реально видны (WRIMP), позволяет фильтровать «слепые зоны» и улучшать UX
  • Фиксация кликов и показов даёт системе обратную связь: что работает, а что нет

Когда вы используете кастомный рендеринг (JSON-кампании) или создаете собственные виджеты продуктов (ProductWidgetBuilder), SDK не может автоматически отслеживать взаимодействия пользователя с вашим UI. В этих случаях вы должны вручную отправлять события, чтобы Gravity Field мог корректно измерять эффективность кампаний.

Существует два основных типа событий взаимодействия:

  1. ContentEngagement: Относится ко всей кампании или виджету в целом (например, показ баннера).
  2. ProductEngagement: Относится к конкретному продукту внутри виджета (например, клик по карточке товара).

Пример: Ручное отслеживание клика по товару в ProductWidgetBuilder

class CustomProductWidgetBuilder extends ProductWidgetBuilder {
  @override
  Widget build({
    required BuildContext context,
    required Slot product,
    required CampaignContent content,
    required Campaign campaign,
  }) {
    return GestureDetector(
      onTap: () {
        // 1. Отправляем событие клика
        GravitySDK.instance.sendProductEngagement(
          ProductClickEngagement(product, content, campaign),
        );

        // 2. Выполняем навигацию на страницу товара
        // your_navigation_logic(product.item.url);
      },
      child: Card(
        // ... ваша верстка карточки товара
      ),
    );
  }
}

Пример: Ручное отслеживание видимости для JSON-кампании

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

import 'package:visibility_detector/visibility_detector.dart';

// В вашем кастомном виджете, который рендерит JSON-кампанию

@override
Widget build(BuildContext context) {
  // ... логика получения campaign и content ...

  return VisibilityDetector(
    key: Key(content.contentId), // Уникальный ключ для виджета
    onVisibilityChanged: (visibilityInfo) {
      if (visibilityInfo.visibleFraction >= 0.5) {
        // Если виджет виден на 50% или более, отправляем событие
        GravitySDK.instance.sendContentEngagement(
          ContentVisibleImpressionEngagement(content, campaign),
        );
      }
    },
    child: YourCustomCampaignUI(
      // ...
    ),
  );
}

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

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

# AddToCartEvent

Событие для отслеживания добавления товара в корзину.

Свойство Тип Обязательное Описание
value double Да Общая стоимость добавленных товаров.
productId String Да SKU/ID добавленного товара.
quantity int Да Количество единиц добавленного товара.
currency String? Нет Код валюты (например, "RUB").
cart List? Нет Полный состав корзины после добавления.

Пример:

final event = AddToCartEvent(
  value: 1500.00,
  productId: 'sku-abc-1',
  quantity: 1,
  currency: 'RUB',
  cart: [
    CartItem(productId: 'sku-abc-1', quantity: 1, itemPrice: 1500.00),
    CartItem(productId: 'sku-xyz-3', quantity: 2, itemPrice: 500.00),
  ],
);

# PurchaseEvent

Событие для отслеживания успешной покупки.

Свойство Тип Обязательное Описание
uniqueTransactionId String Да Уникальный идентификатор транзакции/заказа.
value double Да Общая стоимость заказа.
currency String? Нет Код валюты.
cart List? Нет Состав купленных товаров.

Пример:

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

# RemoveFromCartEvent

Событие для отслеживания удаления товара из корзины.

Свойство Тип Обязательное Описание
value double Да Общая стоимость удаленных товаров.
productId String Да SKU/ID удаленного товара.
quantity int Да Количество единиц удаленного товара.
currency String? Нет Код валюты.
cart List? Нет Полный состав корзины после удаления.

Пример:

final event = RemoveFromCartEvent(
  value: 500.00,
  productId: 'sku-xyz-3',
  quantity: 1,
  currency: 'RUB',
  cart: [
    CartItem(productId: 'sku-abc-1', quantity: 1, itemPrice: 1500.00),
  ],
);

# SyncCartEvent

Событие для синхронизации полного состава корзины пользователя.

Свойство Тип Обязательное Описание
currency String? Нет Код валюты.
cart List? Нет Актуальный состав корзины.

Пример:

final event = SyncCartEvent(
  currency: 'RUB',
  cart: [
    CartItem(productId: 'sku-abc-1', quantity: 1, itemPrice: 1500.00),
    CartItem(productId: 'sku-xyz-3', quantity: 2, itemPrice: 500.00),
  ],
);

# AddToWishlistEvent

Событие для отслеживания добавления товара в список желаний.

Свойство Тип Обязательное Описание
productId String Да SKU/ID добавленного товара.

Пример:

final event = AddToWishlistEvent(
  productId: 'sku-fav-789',
);

# SignUpEvent

Событие для отслеживания регистрации нового пользователя.

Свойство Тип Обязательное Описание
hashedEmail String Да Хешированный email пользователя (например, SHA-256).
cuid String Да Уникальный ID пользователя в вашей системе.
cuidType String Да Тип идентификатора (например, 'customer_id').

Пример:

final event = SignUpEvent(
  hashedEmail: 'a1b2c3d4...', // SHA-256 от email
  cuid: 'user-555-new',
  cuidType: 'internal_user_id',
);

# LoginEvent

Событие для отслеживания входа пользователя в систему. Ключевое событие для склейки анонимного и авторизованного профилей.

Свойство Тип Обязательное Описание
hashedEmail String? Нет Хешированный email пользователя.
cuid String? Нет Уникальный ID пользователя в вашей системе.
cuidType String? Нет Тип идентификатора.

Пример:

final event = LoginEvent(
  cuid: 'customer-12345',
  cuidType: 'mySystemUserId',
);

# CustomEvent

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

Свойство Тип Обязательное Описание
type String Да Уникальный системный тип события (например, 'survey-completed-v1').
name String Да Человекочитаемое имя события (например, 'Опрос пройден').
properties Map<String, String>? Нет Дополнительные параметры в формате ключ-значение.

Пример:

final event = CustomEvent(
  type: 'video-watched-v1',
  name: 'Просмотр видео',
  properties: {
    'videoId': 'promo-video-2025',
    'watchDuration': '125s',
  },
);

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

SDK предоставляет механизм обратных вызовов (callbacks) для информирования вашего приложения о различных событиях, происходящих внутри. Это критически важно для реализации интерактивных элементов в In-App кампаниях, таких как переходы по ссылкам, диплинкам или запрос разрешений.

Вы можете подписаться на эти события, передав функцию gravityEventCallback при инициализации SDK.

# Обзор событий (TrackingEvent)

TrackingEvent — это запечатанный (sealed) класс, который объединяет все возможные события. Ниже представлена таблица со всеми типами событий и их описанием.

Событие (Event) Описание Полезные данные
ContentLoadEvent Контент кампании был успешно загружен. content, campaign
ContentImpressionEvent Контент кампании был показан (начало рендера). content, campaign
ContentVisibleImpressionEvent Контент кампании стал видимым на экране (>=50% площади). content, campaign
ContentCloseEvent Пользователь закрыл In-App сообщение. content, campaign
CopyEvent Пользователь скопировал данные (например, промокод). copiedValue, content, campaign
CancelEvent Пользователь отменил действие (например, нажал "Отмена"). content, campaign
FollowUrlEvent Пользователь нажал на элемент, ведущий на внешний URL. Требует обработки. url, content, campaign
FollowDeeplinkEvent Пользователь нажал на элемент, ведущий на внутренний экран приложения. Требует обработки. deeplink, content, campaign
RequestPushEvent Пользователь нажал на элемент с запросом на подписку на пуш-уведомления. Требует обработки. content, campaign
ProductImpressionEvent Карточка товара в блоке рекомендаций стала видимой на экране. slot, content, campaign

# Примеры обработки ключевых событий

Некоторые события, такие как FollowUrlEvent или FollowDeeplinkEvent, требуют обязательной обработки в вашем приложении для обеспечения полной функциональности. SDK лишь сообщает о намерении пользователя, а само действие (открытие ссылки, переход на экран) должно быть реализовано на стороне вашего кода.

# Обработка FollowUrlEvent

Это событие возникает, когда пользователь нажимает на кнопку или элемент, который должен вести на внешний веб-сайт.

Реализация: Для открытия URL рекомендуется использовать популярный пакет url_launcher.

  1. Добавьте url_launcher в ваш pubspec.yaml:
dependencies:
  url_launcher: ^6.3.1
  1. Обработайте событие в gravityEventCallback:
import 'package:url_launcher/url_launcher.dart';

// ... в коде инициализации SDK
gravityEventCallback: (event) {
  if (event is FollowUrlEvent) {
    final uri = Uri.parse(event.url);
    // Проверяем, можем ли мы открыть URL
    canLaunchUrl(uri).then((canLaunch) {
      if (canLaunch) {
        // Открываем ссылку во внешнем приложении (браузере)
        launchUrl(uri, mode: LaunchMode.externalApplication);
      } else {
        print('Could not launch ${event.url}');
      }
    });
  }
}

# Обработка FollowDeeplinkEvent

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

Реализация: Логика обработки диплинков полностью зависит от архитектуры навигации в вашем приложении. SDK лишь передает строку диплинка, которую вы должны обработать с помощью вашего роутера (GoRouter, Navigator 2.0, auto_route и т.д.).

Концептуальный пример с GoRouter:

// ... в коде инициализации SDK
gravityEventCallback: (event) {
  if (event is FollowDeeplinkEvent) {
    // Предполагается, что у вас есть доступ к BuildContext
    // или вашему роутеру для выполнения навигации.
    // Например, если вы используете GoRouter:
    // final router = GoRouter.of(context);
    // router.go(event.deeplink); // например, '/product/12345'
    print('Navigate to deeplink: ${event.deeplink}');
  }
}

💡 Совет: Если у вас нет прямого доступа к BuildContext или роутеру в месте инициализации SDK, вы можете использовать StreamController или другой state management подход, чтобы передать событие навигации в UI-слой вашего приложения.

# Обработка RequestPushEvent

Это событие возникает, когда пользователь выражает желание подписаться на пуш-уведомления, нажав на соответствующий элемент в In-App кампании.

Реализация: В ответ на это событие ваше приложение должно запустить собственную логику запроса разрешений на отправку пуш-уведомлений. SDK не управляет разрешениями напрямую.

Концептуальный пример с firebase_messaging:

import 'package:firebase_messaging/firebase_messaging.dart';

// ... в коде инициализации SDK
gravityEventCallback: (event) {
  if (event is RequestPushEvent) {
    // Запускаем логику запроса разрешений
    requestPushPermissions();
  }
}

Future<void> requestPushPermissions() async {
  final messaging = FirebaseMessaging.instance;
  final settings = await messaging.requestPermission();
  
  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    print('User granted permission');
    // Можно отправить событие об успешной подписке в вашу аналитику
  } else {
    print('User declined or has not accepted permission');
  }
}