# 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.8 # Замените на актуальную версию
  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`:
```yaml
dependencies:
  url_launcher: ^6.3.1

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

# Шаг 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 автоматически покажет ее.

# Почему 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',
    ),
  );
}

# Шаг 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,
});
  • apiKey (required): Ваш уникальный ключ API.
  • section (required): Идентификатор секции вашего проекта.
  • productWidgetBuilder: Опциональный билдер для кастомизации виджетов продуктов. (См. раздел Работа с контентом).
  • 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'], // Запросить конкретные поля
  ),
);

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

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

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

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

Чтобы связать анонимный профиль с профилем авторизованного пользователя, после входа в систему отправьте 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',
    ),
  );
}

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

Если ваше приложение уже управляет 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.

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

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

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

Свойство Тип Описание
type ContextType Тип страницы (homepage, product, cart и т.д.).
data List Массив строк с данными, зависящими от типа (например, SKU для product).
location String Уникальный идентификатор местоположения (URL, deeplink, название экрана).
lng String? Язык или регион.
utm Map<String, String>? UTM-метки.
attributes Map<String, Object>? Дополнительные атрибуты для таргетинга.

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

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

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

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

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

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

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

# triggerEvent(...)

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

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

Подробное описание всех доступных событий и их параметров — в разделе ниже.

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

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

# setNotificationPermissionStatus()

Устанавливает текущий статус разрешения на push-уведомления.

void setNotificationPermissionStatus(NotificationPermissionStatus status);

status: NotificationPermissionStatus.granted, NotificationPermissionStatus.denied или NotificationPermissionStatus.unknown.

Пример получения и установки статуса (через firebase_messaging):

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:gravity_sdk/gravity_sdk.dart';

// 1. Получаем статус из системы
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;
  }
}

// 2. Передаем статус в SDK при старте приложения и после запроса разрешений
final status = await getNotificationStatus();
GravitySDK.instance.setNotificationPermissionStatus(status);

# FAQ / Troubleshooting

  1. Почему GravityInlineWidget не отображается или имеет нулевую высоту?

    • Вы не задали обязательный параметр height. Задайте высоту в конструкторе GravityInlineWidget.
  2. Почему UI кампании выглядит иначе, чем в превью?

    • SDK наследует стили из ThemeData приложения. Если в кампании не задан шрифт/цвет, используется Theme.of(context).
  3. Почему клик по кнопке в кампании не приводит к переходу?

    • Не обработаны события FollowUrlEvent/FollowDeeplinkEvent в gravityEventCallback. SDK не выполняет навигацию сам.

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

# GravityInlineWidget (Автоматический рендеринг)

Виджет для встраивания кампаний (например, товарных рекомендаций) прямо в верстку вашего экрана.

GravityInlineWidget(
  selector: 'homepage-recommendations',
  height: 250, // Обязательно!
  pageContext: PageContext(
    type: ContextType.homepage,
    data: [],
    location: 'app://homepage',
  ),
)

# ProductWidgetBuilder (Кастомный рендеринг товаров)

Для полного контроля над внешним видом карточек товаров в GravityInlineWidget, создайте свой класс, наследующий ProductWidgetBuilder.

// 1. Создайте свой билдер
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? ?? ''),
      ),
    );
  }
}

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

⚠️ Обратите внимание: product.item представляет собой Map<String, dynamic>. Ключи в этой карте (например, 'name', 'price', 'imageUrl') соответствуют полям в вашем товарном фиде. Убедитесь, что вы используете правильные ключи и безопасно обрабатываете типы данных и возможные null значения.

# JSON-кампании (Полностью ручной рендеринг)

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

⚠️ Важно: При работе с JSON-кампаниями SDK лишь доставляет данные. Ваше приложение несет полную ответственность за парсер JSON-объекта, полученного из response.data, и последующий рендеринг UI на основе этих данных.

# 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) {
      // Здесь ваша логика обработки и отображения данных из event.content
    }
  },
);

💡 Практическое руководство

Хотите увидеть полный пример реализации A/B-теста с UI, управляемым через JSON?

Ознакомьтесь с нашим пошаговым руководством: Гайд: A/B-тестирование в Flutter с использованием JSON. В нем мы на сквозном примере показываем, как связать getContentBySelector и sendContentEngagement для проведения полноценного эксперимента.

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

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

# Зоны ответственности при использовании ProductWidgetBuilder

Когда вы используете ProductWidgetBuilder для кастомного отображения товарных карточек, SDK и ваше приложение делят ответственность за отслеживание событий:

# Что SDK делает автоматически:

  • Показ виджета (ContentImpressionEvent): SDK автоматически фиксирует показ всего inline-блока, как только он загружается.
  • Видимость виджета (ContentVisibleImpressionEvent): SDK отслеживает, когда весь блок с рекомендациями становится видимым на экране пользователя (попадает во viewport).
  • Видимость карточки товара (ProductImpressionEvent): SDK автоматически отслеживает, когда конкретная карточка товара, отрисованная вашим билдером, становится видимой на экране.

# Что должен реализовать разработчик:

  • Клик по товару (ProductClickEngagement): SDK не может знать о внутренней структуре вашего кастомного виджета. Поэтому вы обязаны вызывать метод GravitySDK.instance.sendProductEngagement() внутри обработчика нажатия на вашу карточку товара. Без этого клики не будут засчитаны.

Пример реализации в ProductWidgetBuilder:

      onTap: () {
  // 1. Отправляем событие клика в Gravity Field
        GravitySDK.instance.sendProductEngagement(
    ProductClickEngagement(product, content, campaign)
  );
  // 2. Выполняем навигацию на экран товара
  // your_navigation_logic(product.item['url']);
},

# sendContentEngagement()

Отправляет событие, относящееся ко всей кампании (например, показ баннера).

# sendProductEngagement()

Отправляет событие, относящееся к конкретному продукту внутри кампании (например, клик по товару).

Пример:

// Отправка клика по товару
        GravitySDK.instance.sendProductEngagement(
          ProductClickEngagement(product, content, campaign),
        );

// Отправка показа кастомного баннера (используйте VisibilityDetector)
        GravitySDK.instance.sendContentEngagement(
          ContentVisibleImpressionEngagement(content, campaign),
        );

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

В таблице ниже описаны основные классы событий, которые можно отправлять с помощью triggerEvent(...).

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

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

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

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

Событие Описание Требует обработки?
ContentLoadEvent Контент кампании загружен. Нет
ContentImpressionEvent Контент кампании показан. Нет
ContentVisibleImpressionEvent Контент стал видимым на экране. Нет
ContentCloseEvent Пользователь закрыл In-App. Нет
CopyEvent Пользователь скопировал данные (промокод). Нет
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-уведомления
  }
}

Чтобы видеть клики/навигацию в отчетах, можно дополнительно отправлять CustomEvent через triggerEvent(...) в обработчиках колбэка.

# FAQ (Часто задаваемые вопросы)

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

Q: Почему мой GravityInlineWidget не отображается или имеет нулевую высоту?
A: Самая частая причина — для GravityInlineWidget не задан параметр height. Flutter требует явных ограничений по размеру для многих виджетов. Убедитесь, что вы передали конкретное значение высоты: height: 250.

Q: Как обрабатывать клики по кнопкам и ссылкам в In-App сообщениях (например, в модальном окне)?
A: SDK отправляет события FollowUrlEvent (для внешних ссылок) и FollowDeeplinkEvent (для внутренней навигации) в колбэк gravityEventCallback, который вы передаете при инициализации. Вы должны реализовать логику обработки этих событий, например, используя пакет url_launcher для открытия ссылок. SDK сам навигацию не выполняет.

Q: Что произойдет, если SDK не сможет получить кампанию от сервера (например, нет интернета)?
A: SDK обработает ошибку внутри. Если это GravityInlineWidget, он просто не будет отображен (останется пустым). Если это In-App кампания, она не будет показана. Ошибки не должны приводить к падению приложения.

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

Q: Как мне протестировать кампанию перед запуском на всех пользователей?
A: Используйте таргетинг по CUID (Custom User ID) в настройках кампании в интерфейсе Gravity Field. Укажите свой тестовый ID, и кампания будет показываться только вам.