Вступление

Всем привет, на связи Василий Боровой и Иван Козловский – Flutter-разработчики из The Head. В этой статье хотим поделиться с вами опытом работы над YPay и YPay inventory для Flutter, рассказать про возможности библиотек и как их использовать, а также о проблемах, с которыми столкнулись.

Посмотреть исходники можно на нашем GitHub. ypay и ypay_inventory уже доступны на pub.dev

В полезных ссылках будет указано все необходимое для желающих подробнее узнать о Яндекс Пэй. Быть может после прочтение появится желание интегрировать библиотеки в свое мобильное приложение.

Небольшая предыстория:

Мы достаточно давно работаем над мобильным приложением ювелирной сети ADAMAS и до недавних пор в нем присутствовала оплата через YPay с возможностью оформления заказа в Сплит. Происходило это, конечно же, через WebView. Для этого мы даже реализовали отдельный флоу офомления быстрого заказа, но на этом хорошее для нас и пользователей быстро закончилось, поскольку процесс оплаты был не совсем предсказуемым и далеко не самым удобным. Использование WebView имеет место быть, конечно, но оно накладывает разного рода ограничения, а также не является безопасным, а иногда и вовсе приходится идти на компромиссы и откровенно костылить. Если кто-то реализовывал какие то флоу оплаты через WebView, возможно, поймут нас. С другой стороны интеграция SDK благоприятно сказывается на производительности, безопасности и UX, а также делает процесс прогнозируемым и прозрачным для всех сторон. Но так как готового решения для Flutter не было мы планировали дождаться порт от коллег из Яндекса, но немного не успели. В июне 2024 нам сообщили о прекращении поддержки оплат через WebView и рекомендовал всем партнерам перейти на их нативное SDK. Посовещавшись, мы решили реализовать свое собственное решение, чтобы и дальше поддерживать этот вид оплаты. За основу мы взяли ключевые возможности нативных SDK.

Статья разбита на две части:

  1. Расскажем про YPay

  2. Расскажем про YPay inventory

1. Yandex Pay

YPay — это платёжный сервис от Яндекса, который позволяет интегрировать систему оплаты в мобильные приложения.

Как мы организовали работу:

  • изучили документацию нативных sdk для ios и android, важно было понимать какие методы нужно будет реализовывать и сколько их;

  • нашли example приложения на kotlin и swift и изучили их. Забегая вперед скажу, что example для android очень упростил работу, а вот для ios...;

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

  • реализовали интерфейс и контракты;

  • реализовали android и ios модули;

  • частично покрыли тестами;

  • написали простенький example;

  • написали документацию и особенности работы с библиотекой.

Модули:

  • ypay_platform_interface - этот модуль содержит абстракции и контракты для взаимодействия между Flutter и нативными платформами. Здесь находятся определения интерфейсов, которые реализуются в модулях для iOS и Android;

  • ypay_ios и ypay_android - модули реализуют работу с нативными SDK для iOS и Android соответственно. Они включают в себя имплементацию методов YPay для платформ, управление контрактами, обработку событий и конфигурацию sdk;

  • ypay - основной Flutter-модуль, предоставляющий удобный и унифицированный API для интеграции. Все основные вызовы проходят через этот модуль, что позволяет сосредоточиться на конкретных возможностях библиотеки, не погружаться в лишние детали. Тут же реализован готовый контракт создания платежа и получения его результата, который позволяет абстрагироваться от дополнительной логики.

Аналогично библиотеке ypay мы реализовали и ypay inventory

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

Что получилось:

В результате проделанной работы мы получили первую версию библиотеки, которая и решила нашу задачу – сохранить оплату через Яндекс в приложении, еще и сделав ее куда более быстрой и удобной, требующий от пользователя всего пару кликов. На сегодняшний день мы обновились до последних версий и реализовали инвентарь виджетов, о котором расскажем немного позже.

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

Для кого это будет полезно:

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

Кратко о возможностях библиотек:

  • инициализация и настройка конфигурации;

  • оплата покупок по платежной ссылке;

  • получение и обработка результата платежа в реальном времени.

  • контракт для упрощения процесса оплаты и обработки результата;

  • поддержка нативных виджетов из бибилиотек инвентаря;

  • поддержка нативных бейджей из бибилиотек инвентаря;

  • кастомизация виджетов и бейджей аналогичная нативным библиотекам;

  • обработка кликов у виджетов.

На что стоит обратить внимание перед интеграцией:

  • ypay 1.0.3 и ypay_inventory 1.0.3 реализуют нативную версию sdk iOS - 1.13.0;

  • ypay 1.0.3 и ypay_inventory 1.0.3 реализуют нативную версию sdk Android - 2.3.10;

  • минимальная версия android – 24;

  • минимальная версия ios – 14.0;

  • поддержка AppLinks в приложении – оф. документации по AppLinks для Android и iOS а также Huawei;

  • убедитесь, что установлена тема для Application и для Aсtivity в AndroidManifest.xml;

  • рекомендуем использовать android:launchMode="singleTask" если приложение поддерживает App Links;

  • проверьте от чего наследуется тема в android/app/src/main/res/values/styles.xml, необходимо использовать Theme.MaterialComponents или Theme.AppCompat;

  • MainActivity должен наследоваться от FlutterFragmentActivity() в MainActivity.kt.

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

Интеграция библиотеки в приложение

Для наглядности и лучшего понимания рекомендуем ознакомиться с нашим example приложением. Полный процесс подключения описан на странице библиотеки.

Подключаем зависимость в pubspec.yaml:

dependencies:  
  ypay: 1.0.3

Далее инициализируем sdk:

ypayPlugin.init(
  configuration: const Configuration(
    // Уникальный идентификатор продавца, который предоставляется при регистрации продавца в сервисе Яндекс Пэй. Его можно получить в настройках консоли Яндекс Пэй.
    merchantId: 'your merchant id',
    // Название продавца, которое будет отображаться пользователю во время платежной операции.
    merchantName: 'Demo Merchant',
    // URL-адрес продавца, который будет отображаться пользователю в процессе оплаты.
    merchantUrl:` "https://example.ru/",
    // Указывает, в каком режиме будет работать YPay. Если true, используется тестовая среда (SANDBOX); если false, используется прод среда (PRODUCTION).
    testMode: true,
  ),
);

Создаем контракт:

final contract = YPayContract.create(
 url: url,
 onStatusChange: (contract, result) {
  switch (result.status) {
    case YPayStatus.none:
    case YPayStatus.cancelled:
    case YPayStatus.failure:
    /// Не успешная обработка
    case YPayStatus.success:
    /// Успешная обработка
  }
 },
);

/// Запуск оплаты
contract.pay();

Не забудьте закрыть закрыть контракт внутри обработчика в случае ошибок или успеха:

  /// Закрытие контракта
  contract.cancel();

Статусы оплаты и возможные ошибки:

"Finished with success" - успешный платеж 
"Finished with cancelled event" - отмена платежа
"Finished with domain error" - ошибка платежа
"Finished when contract is null" - ошибка платежа

Одновременно может быть запущен только один контракт. Также стоит обратить внимание, что если у пользователя не установлено ни одно приложение от Яндекса, которое поддерживает SDK оплаты, то его перенаправит во внешний браузер (или же webview). В таком случае результат платежа все равно должен корректно возвращаться, однако 100% это гарантировать на будем. Но даже такой случай можно безболезненно обработать.

Через контракт можно также получать информацию о состоянии транзакции в реальном времени. Если вам не подходит контракт, то есть возможность запускать оплату вручную и подписываться на результат платежа. Для этого используйте методы, подробнее есть в документации на странице пакета.

// Принимает ссылку на оплату
startPayment({required String url})
// Возвращает результат платежа
paymentResultStream()

2. Yandex Pay Inventory

Инвентарь — это набор визуальных элементов с брендированными продуктами Яндекса. На Android и IOS сейчас доступны бейджи и виджеты.

Виджеты — это элементы интерфейса, которые сообщают пользователю о возможности оплатить покупку в рассрочку через Сплит или получить кешбэк. В SDK реализованы виджеты трех типов:

iOS

Android

Item-виджет

SimpleWidget

Checkout-виджет

InfoWidget

BNPL-виджет

BnplPreviewWidget 

Помимо названий виджетов есть и отличия в именовании аргументов. Android нам показалcя ближе и мы приняли решение придерживаться нейминга в соответствии с Android sdk.

Информация по инвентарю:

К бейджам относится YPayBadge; к виджетам - YPaySimpleWidgetView, YPayInfoWidgetView и YPayBnplPreviewWidgetView.

  • все бейджи и виджеты - это нативные view, которые показываются через PlatformView; 

  • в каждый бейдж или виджет необходимо передать сумму, то есть стоимость товара;

  • у каждого бейджа и виджета можно изменить тему. По умолчанию системная;

  • все виджеты имеют минимальную ширину равную 280 pt.

Подробное описание виджетов и вариаций кастомизации, а также описание всех полей, рекомендуем посмотреть на официальной странице документации от Яндекса. Как уже было сказано выше – именование параметров как в Android.

Интеграция библиотеки в приложение

Подключаем зависимость в pubspec.yaml:

dependencies:  
  ypay_inventory: 1.0.3

Инициализируйте инвентарь:

ypayInventoryPlugin.init(
  configuration: const Configuration(
      // ваш Merchant ID
      merchantId: 'your merchant id',
      // название вашего магазина
      merchantName: 'Demo Merchant',
      // ссылка на ваш магазин
      merchantUrl: "https://example.ru/",
      // [необзятально] режим отладки (по умолчанию - true)
      testMode: true,
      // [необзятально] режим скрытия бейджей в случае отсутствия данных (по умолчанию - YPayBadgeHidingPolicy.gone)
      badgeHidingPolicy: YPayBadgeHidingPolicy.gone,
    ),
  );

Основные виджеты:

YPayBadge

Виджет для показа бейджей.
Есть два типа виджетов - кэшбэк и сплит. CashbackBadgeRenderData вернет бейдж с кэшбэком, SplitBadgeRenderData вернет бейдж со сплитом.

/// Бейдж кэшбэка
return YPayBadge(
	sum: 1230,
	width: 200,
	renderData: CashbackBadgeRenderData(
		// Тема виджета: светлая (YPayWidgetTheme.light), или темная (YPayWidgetTheme.dark), или системная (YPayWidgetTheme.system)
		theme: YPayBadgeTheme.system,
		// Выравнивание бейджа относительно контейнера: (YPayBadgeAlign.left, YPayBadgeAlign.center, YPayBadgeAlign.right)
		align: YPayBadgeAlign.left,
		// Цвет бейджа: (SplitBadgeColor.primary, SplitBadgeColor.green, SplitBadgeColor.grey, SplitBadgeColor.transparent)
		color: CashbackBadgeColor.primary,
		// Версия бейджа
		variant: CashbackBadgeVariant.detailed,
	),
);
/// Бейдж сплита
return YPayBadge(
	sum: 1230,
	width: 200,
	renderData: SplitBadgeRenderData(
		// Тема виджета: светлая (YPayWidgetTheme.light), или темная (YPayWidgetTheme.dark), или системная (YPayWidgetTheme.system)
		theme: YPayBadgeTheme.system, 
		// Выравнивание бейджа относительно контейнера: (YPayBadgeAlign.left, YPayBadgeAlign.center, YPayBadgeAlign.right)
		align: YPayBadgeAlign.left,
		// Цвет бейджа: (SplitBadgeColor.primary, SplitBadgeColor.green, SplitBadgeColor.grey, SplitBadgeColor.transparent)
		color: SplitBadgeColor.primary,
		// Версия бейджа, только для типа со Сплитом
		variant: SplitBadgeVariant.simple,
	),
);

YPaySimpleWidgetView

По умолчанию содержит в себе два блока: Сплит и баллы Плюса. На каждый блок можно нажать и перейти на страницу соответствующего лендинга с более подробной информацией о продуктах Сплита и Плюса.

return YPaySimpleWidgetView(
	// Сумма заказа
	sum: 10000,
	renderData: SimpleWidgetRenderData(
		// Настройки прозрачности: сплошной (YPayWidgetStyle.solid) или прозрачный (YPayWidgetStyle.transparent)
		style: YPayWidgetStyle.solid,
		// Тема виджета: светлая (YPayWidgetTheme.light), или темная (YPayWidgetTheme.dark), или системная (YPayWidgetTheme.system)
		theme: YPayWidgetTheme.system,
		// Тип данных в виджете: Сплит, кешбэк или оба варианта
		types: {YPayWidgetType.split, YPayWidgetType.cashback},
	),
);

YPayInfoWidgetView

Отображение можно настроить так, чтобы показывался только сплит, только кэшбэк или три блока сразу. По умолчанию содержит в себе три секции: баллы Плюса, BNPL-план Сплита и информацию об оплате без первоначального взноса.

return YPayInfoWidgetView(
	// Сумма заказа
	sum: 1230,
	renderData: InfoWidgetRenderData(
		// Тема виджета: светлая (YPayWidgetTheme.light), или темная (YPayWidgetTheme.dark), или системная (YPayWidgetTheme.system)
		theme: YPayWidgetTheme.system,
		// Тип данных в виджете: Сплит, кешбэк или оба варианта
		types: {YPayWidgetType.split, YPayWidgetType.cashback},
	),
);

YPayBnplPreviewWidgetView

Кастомизируемый BNPL-виджет позволяет предварительно ознакомиться с условиями доступных некредитных планов Сплит и выбрать подходящий.
Виджет состоит из четырех частей:

  • Кликабельная шапка с логотипом Сплит и краткой информацией о платежах и комиссии выбранного плана;

  • Селектор планов (отображается, если пользователю доступно более одного плана Сплита);

  • Информация о платежах по датам;

  • Кнопка «Оформить» (по умолчанию скрыта).

Для шапки и кнопки «Оформить» можно задать свои функции на клик.

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

return YPayBnplPreviewWidgetView(
	// Сумма заказа
    sum: 1230,
	// Слушатель клика по шапке виджета (при установленном YPayWidgetHeader.standard)
	onHeaderClick: () {
	// Показ информации об оплате частями
	},
	// Слушатель клика по кнопке «Оформить» (параметр selectedPlan содержит количество месяцев выбранного плана Сплита)
	onCheckoutButtonClick: (int selectedPlan) {
	// Переход на экран оплаты
	},    
	renderData: BnplPreviewWidgetRenderData(
		// Тема виджета: светлая (YPayWidgetTheme.light), или темная (YPayWidgetTheme.dark), или системная (YPayWidgetTheme.system)
		theme: YPayWidgetTheme.system,
		// Тип отображения шапки виджета: стандартный (YPayWidgetHeader.standard) или уменьшенный (YPayWidgetHeader.minified)
		header: YPayWidgetHeader.standard
		// Фон виджета: стандартный (YPayWidgetBackground.standard), прозрачный (YPayWidgetBackground.transparent) или произвольный (YPayWidgetBackground.custom)
		background: YPayWidgetBackground.standard,
		// Наличие обводки виджета
		hasOutline: true
		// Радиус виджета в пикселях
		radius: 30
		// Наличие внутреннего отступа виджета
		hasPadding: true
		// Размер виджета: средний (YPayWidgetSize.medium) или маленький (YPayWidgetSize.small)
		size: YPayWidgetSize.medium,
		// Наличие кнопки «Оформить»
		hasCheckoutButton: false,
		// Цвет фона виджета (при установленном YPayWidgetBackground.custom)
		backgroundColor: 0x000000,
	),
);
Упрощенное представление вышеописанных виджетов
Упрощенное представление вышеописанных виджетов

Под конец статьи решили оставить информацию по работе с размерами в нативе. Мы в процессе разработки столкнулись с проблемой, при которой platform view некорректно отображает и обновляет размеры виджетов. В целом в документации Яндекса есть некоторая информация по размерам, но все же пришлось немного углубиться. В итоге все получилось и вроде бы даже неплохо. 

В текущем формате не будем максимально подробно описывать все нюансы, но мы постарались сжать полученную информацию и поделиться с вами. Это помогло нам решить наши проблемы с PlatformView, может, и вам когда-нибудь пригодится. Спрячем это под спойлер)

Скрытый текст

Справка по работе размеров Android:

Виджеты могут занимать различную высоту или даже скрываться и важно корректно это учитывать. Базово высота виджета равна 0, через контроллер с натива возвращается и устанавливается текущая высота. 

Размеры бэйджей строятся исходя из соотношения указанного в нативе: 

  • если высота установлена, то ширина рассчитывается автоматически с учетом соотношения сторон. При этом установленная ширина не учитывается;

  • если высота не установлена, но указана ширина, то высота рассчитывается автоматически с учетом соотношения сторон.

На андроиде при проверке занимаемых размеров виджета, единственный виджет, у которого пришлось отдельно учитывать видимость, был BnplPreviewWidget, остальные виджеты становились Visibility.GONE или просто скрывали контент при отсутствии данных к показу.

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

GlobalLayoutListener в Android — это интерфейс, который предоставляется в рамках системы событий ViewTreeObserver. Он позволяет отслеживать изменения глобального макета в приложении, например, когда изменяется размер или положение элементов интерфейса, или когда добавляются новые элементы в дерево представлений.

LayoutChangeListener в Android — это интерфейс, который используется для отслеживания изменений размеров и положения конкретного View в рамках его родительского контейнера. Позволяет вам реагировать на изменения макета (например, изменение размеров, положения или других параметров) конкретного элемента в иерархии представлений.

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

Рекомендации:

  • Используйте OnLayoutChangeListener, если вам нужно точно отслеживать изменения конкретного View.

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

  • Для единоразового получения размеров используйте post().

  • Если есть возможность напрямую отслеживать состояние данных влияющих на отображение или используете кастомное вью, лучше пойти другим путем

Справка по работе размеров iOS:

  1. Цепочка вызовов onSizeChanged начинается с изменения размеров виджета:

    • Это происходит, когда виджет обновляется, например, при вызове метода updateView(args:) в UIViewController или при первичной загрузке через viewDidLoad().

  2. Обновление размеров в updateSize:

    • В методе updateSize(newView:) для нового подвиджета вызывается sizeThatFits(_:), который рассчитывает подходящий размер на основе модели.

    • Виджету задается новый фрейм (frame) с рассчитанными размерами.

  3. Система вызывает viewDidLayoutSubviews:

    • После изменения размеров или обновления дочерних представлений UIKit вызывает viewDidLayoutSubviews(). Этот метод вызывается всякий раз, когда UIView завершает размещение своих подвидов.

    • Внутри viewDidLayoutSubviews() происходит измерение фактического размера дочернего виджета (self.view.subviews.first!).

  4. Отправка данных о размере в Flutter:

    • После получения нового размера (ширина и высота), viewDidLayoutSubviews() отправляет сообщение в Flutter через канал (channel.invokeMethod("onSizeChanged", arguments: [...])).

    • В Flutter сообщение обрабатывается, чтобы синхронизировать размеры между нативной частью и Flutter.

Основные компоненты цепочки:

  1. Метод updateView(args:):

    • Обновляет свойства модели виджета.

    • Заменяет текущий виджет на новый, пересчитанный на основе новых параметров.

  2. Метод updateSize(newView:):

    • Добавляет новый подвид в иерархию.

    • Использует sizeThatFits() для расчета размеров.

    • Задает новый фрейм для подвиджета.

  3. Метод viewDidLayoutSubviews:

    • Отслеживает изменения в размещении подвидов.

    • Определяет актуальные размеры виджета и передает их в Flutter.

  4. Вызов onSizeChanged через канал:

    • Отправляет информацию о размере виджета обратно в Flutter, чтобы синхронизировать интерфейс.

Когда вызывается viewDidLayoutSubviews?

  1. При добавлении или удалении подвидов.

  2. При изменении фрейма (frame) или границ (bounds) самого контроллера или его подв��дов.

При вызове методов, изменяющих макет, например, setNeedsLayout или layoutIfNeeded.

Теперь о наболевшем, а конкретно о проблемах, с которыми мы столкнулись:

  • очень сжатые сроки, из-за чего не получились некоторые моменты довести до ума;

  • немного боли при работе с нативом, особенно с ios;

  • ограничения минимальной версии ios. На момент разработки это была ios 13, а сейчас 14, вроде бы и адекватно на 2025, но в то же время у андроида 7.0;

  • в первой версии не работал hot reload на ios. Повторная инициализация sdk крашила приложение, поэтому отвалился hot reload, пришлось временно костылить. Сейчас все хорошо;

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

Где посмотреть пример работы:

  • можете скачать мобильное приложение ADAMAS, там библиотека интегрирована вот уже 5 месяцев, однако для тестирования придется оформлять заказы;

  • также, если в вашем проекте имеются планы по добавлению YPay, то вы можете достаточно быстро организовать тестовую версию своего приложения с нашей библиотекой;

  • в крайнем случае можно запустить example, но нужны тестовые креды.

Наши планы на ближайшее время:

  • начать чаще делиться с сообществом результатами нашей работы и чем-то не менее интересным;

  • написать больше тестов для YPay и YPay inventory;

  • поддерживать библиотеки в актуальном состоянии.

Полезные ссылки, которые стоит посетить:

  • pub.dev YPay

  • pub.dev YPay inventory

  • GitHub где можно посмотреть исходники и помочь в развитии библиотек

  • Чатик наш чатик в Telegram, залетайте!

  • Схема работы мобильных SDK - вводная информация по YPay 

  • Описание виджетов и их параметров. Ориентируемся на Android.

  • API для настройки бэкенда

Завершение

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