
Вступление
Всем привет, на связи Василий Боровой и Иван Козловский – 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.
Статья разбита на две части:
-
Расскажем про YPay
-
Расскажем про 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
, необходимо использовать src res styles.xmlTheme.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 сейчас доступны бейджи и виджеты.
Виджеты — это элементы интерфейса, которые сообщают пользователю о возможности оплатить покупку в рассрочку через Сплит или получить
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
Виджет для показа бейджей.
Есть два типа виджетов -
/// Бейдж кэшбэка
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
Отображение можно настроить так, чтобы показывался только сплит, только
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:
-
Цепочка вызовов onSizeChanged начинается с изменения размеров виджета:
-
Это происходит, когда виджет обновляется, например, при вызове метода updateView(args:) в UIViewController или при первичной загрузке через viewDidLoad().
-
-
Обновление размеров в updateSize:
-
В методе updateSize(newView:) для нового подвиджета вызывается sizeThatFits(_:), который рассчитывает подходящий размер на основе модели.
-
Виджету задается новый фрейм (frame) с рассчитанными размерами.
-
-
Система вызывает viewDidLayoutSubviews:
-
После изменения размеров или обновления дочерних представлений UIKit вызывает viewDidLayoutSubviews(). Этот метод вызывается всякий раз, когда UIView завершает размещение своих подвидов.
-
Внутри viewDidLayoutSubviews() происходит измерение фактического размера дочернего виджета (self.view.subviews.first!).
-
-
Отправка данных о размере в Flutter:
-
После получения нового размера (ширина и высота), viewDidLayoutSubviews() отправляет сообщение в Flutter через канал (channel.invokeMethod("onSizeChanged", arguments: [...])).
-
В Flutter сообщение обрабатывается, чтобы синхронизировать размеры между нативной частью и Flutter.
-
Основные компоненты цепочки:
-
Метод updateView(args:):
-
Обновляет свойства модели виджета.
-
Заменяет текущий виджет на новый, пересчитанный на основе новых параметров.
-
-
Метод updateSize(newView:):
-
Добавляет новый подвид в иерархию.
-
Использует sizeThatFits() для расчета размеров.
-
Задает новый фрейм для подвиджета.
-
-
Метод viewDidLayoutSubviews:
-
Отслеживает изменения в размещении подвидов.
-
Определяет актуальные размеры виджета и передает их в Flutter.
-
-
Вызов onSizeChanged через канал:
-
Отправляет информацию о размере виджета обратно в Flutter, чтобы синхронизировать интерфейс.
-
Когда вызывается viewDidLayoutSubviews?
-
При добавлении или удалении подвидов.
-
При изменении фрейма (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
-
Чатик в Telegram для обсуждения библиотек
-
Схема работы мобильных SDK - вводная информация по YPay
-
Описание виджетов и их параметров. Ориентируемся на Android.
-
API для настройки бэкенда
Завершение
На этом, пожалуй, все. Надеемся, что получилось понятно и без большого количества воды. Это наша первая статья, поэтому пока что только ищем свой формат. Спасибо за внимание!
Автор: twojger