YPay & YPay Inventory для Flutter приложений
Вступление
Всем привет, на связи Василий Боровой и Иван Козловский — 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/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:
Цепочка вызовов 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 через канал:
Когда вызывается 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;
поддерживать библиотеки в актуальном состоянии.
Полезные ссылки, которые стоит посетить:
Завершение
На этом, пожалуй, все. Надеемся, что получилось понятно и без большого количества воды. Это наша первая статья, поэтому пока что только ищем свой формат. Спасибо за внимание!