
Всем привет! Меня зовут Мурат Насиров, я Flutter-разработчик в Friflex. Мы разрабатываем мобильные приложения и имеем большой опыт в сфере ритейла. На одном из проектов я столкнулся с внедрением кнопки оплаты через Систему Быстрых Платежей (СБП). В этой статье я хочу поделиться своим опытом и наработками в быстрой интеграции нативных компонентов SDK СБП в кроссплатформенное приложение на Flutter.
Для начала работы нужно иметь специальный .aar бандл для Andorid и .xcframework для iOS. Вся интеграция будет происходить на основании этих двух компонентов. Для этого переходим на сайт НСПК и скачиваем виджет СБП.
1. Создание плагина
Для создания плагина на Flutter используется команда (документация):
flutter create --org plugin.sbp_pay --template=plugin --platforms=android,ios sbp
Где:plugin.sbp_pay — путь до исполняемого файла плагина на Android-стороне--template=plugin — указание, что создается именно плагинsbp — название плагина
2. Внедрение SDK на Android
В версии SBP SDK 1.2
Открыв архив с бандлом, нужно переместить папку repo в папку android:
Далее открываем файл build.gradle, который также находится в этой папке и добавляем:
// Ищем в папке android папку repo String path = project.mkdir("repo").absolutePath rootProject.allprojects { repositories { maven { url "$path" // Добавляем бандл с SDK как maven источник } } } dependencies { // Добавляем зависимость как транзитивную implementation('sbp.payments.sdk:sbp_sdk:1.2@aar') { transitive = true } }
В .aar бандле СБП используется разрешение, которому нужно отдельное обоснование. Чтобы избежать проблем, нужно открыть sbp_sdk-1.2.aar как архив и в AndroidManifest.xml удалить это разрешение:
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
В версии SBP SDK 1.4
Теперь СБП комплектует свой архив немного иначе. В части с файлами для Android сразу находятся .aar, .pom и .xml файлы. Можно использовать тот же путь размещения файлов, как и для версии 1.2:
Соответственно, в месте указания транзитивной зависимости нужно установить версию 1.4 в файле build.gradle. Вышеописанное разрешение на запрос пакетов всех приложений в этой версии убрали.
Теперь можно приступать к написанию кода на стороне плагина. Чтобы сократить недопонимание, прикладываю реализацию главного класса плагина:
SbpPayPlugin.kt
/** SbpPayPlugin */ class SbpPayPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel private lateinit var context: Context private var activity: Activity? = null companion object { @JvmStatic fun registerWith(registrar: PluginRegistry.Registrar) { val channel = MethodChannel(registrar.messenger(), "sbp_pay") channel.setMethodCallHandler(SbpPayPlugin()) } } override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "sbp_pay") channel.setMethodCallHandler(this) context = flutterPluginBinding.applicationContext } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { "init" -> { // Инициализация SDK SBP.init(context) result.success(true) } "showPaymentModal" -> { // СБП с версии 1.2 использует не Activity, а FragmentManager val fragmentManager = (activity as FlutterFragmentActivity).supportFragmentManager try { SBP.showBankSelectorBottomSheetDialog(fragmentManager, call.arguments as String? ) result.success(true) } catch (e: Exception) { // Перехват ошибок плагина result.error("-", e.localizedMessage, e.message) } } else -> result.notImplemented() } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activity = binding.activity } override fun onDetachedFromActivityForConfigChanges() { activity = null } override fun onDetachedFromActivity() { activity = null } }
Теперь со стороны вашего приложения, которое будет использовать этот плагин, нужно изменить в Android-части родительский класс, от которого расширяется главный класс, так как в коде выше указан каст к FlutterFragmentActivity:
sbp_pay/example/android/app/src/main/kotlin/sbp/plugin/sbp_pay_example/MainActivity.kt
class MainActivity: FlutterFragmentActivity()
Помимо перехода на фрагменты, СБП также решили использовать Material 3, поэтому эти стили надо также обновить со стороны приложения. Обычно это LaunchTheme и NormalTheme. Тоже самое делаем для папки стилей values-night, если она есть:
sbp_pay/example/android/app/src/main/res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="LaunchTheme" parent="Theme.Material3.Light.NoActionBar"> <item name="android:windowBackground">@drawable/launch_background</item> </style> <style name="NormalTheme" parent="Theme.Material3.Light.NoActionBar"> <item name="android:windowBackground">?android:colorBackground</item> </style> </resources>
3. Внедрение SDK на iOS
На стороне iOS всё проще — нужно просто добавить папку SBPWidget.xcframework в корень папки ios, а затем добавить SDK как podзависимость:
В файле sbp_pay.podspec перед end нужно добавить этот код:
s.preserve_paths = 'SBPWidget.xcframework/**/*' s.xcconfig = { 'OTHER_LDFLAGS' => '-framework SBPWidget' } s.vendored_frameworks = 'SBPWidget.xcframework'
Теперь осталось отредактировать файл плагина:
SbpPayPlugin.swift
import Flutter import UIKit import SBPWidget public class SbpPayPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "sbp_pay", binaryMessenger: registrar.messenger()) let instance = SbpPayPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { // Запрашиваем при инициализации доступность SDK на iOS case "init": if #available(iOS 13.0, *) { result(true) } else { result(false) } case "showPaymentModal": if #available(iOS 13.0, *) { if let topController = getTopViewController() { do { try SBPWidgetSDK.shared.presentBankListViewController(paymentURL: call.arguments as! String, parentViewController: topController) result(true) } catch (e) { result(FlutterError(code: "PluginError", message: error.localizedDescription, details: nil)) // Перехват ошибок плагина } } else { result(FlutterError(code: "PluginError", message: "SBP: Failed to implement controller", details: nil)) } } else { result(false)) } default: result(FlutterMethodNotImplemented) } } @available(iOS 13.0, *) private func getTopViewController() -> UIViewController? { if var topController = UIApplication.shared.keyWindow?.rootViewController { while let presentedViewController = topController.presentedViewController { topController = presentedViewController } return topController } return nil } }
Как в Android-, так и в iOS-части в документации описаны возможные ошибки и исключения. По желанию их можно передавать через натив в Flutter-приложение.
Мы используем call.arguments как строку, потому что этим аргументом из плагина будет передаваться ссылка на оплату определенного формата (формат ссылки описан в документации с iOS-частью).
Стоит отметить, что новый виджет СБП работает только с iOS 13. Поэтому нужно учитывать целевую аудиторию, которая будет пользоваться вашим приложением.
4. Подключение плагина на стороне Flutter
Теперь, когда реализация написана на двух нативных частях, можно приступать к созданию платформенных сообщений на стороне Dart:
sbp_pay/lib/sbp_pay.dart
sbp_pay.dart
import 'package:flutter/services.dart'; class SbpPay { static const MethodChannel _channel = MethodChannel('sbp_pay'); static bool _wasInitialized = false; /// Флаг доступности [SbpPay] на данном устройстве. /// /// Доступен только после успешного [init], иначе ошибка. static bool get isAvailable => _isAvailable; static late bool _isAvailable; /// Инициализация плагина SbpPay. /// /// Возвращает false, если сервис не поддерживается устройством. static Future<bool> init() async { if (!_wasInitialized) { _isAvailable = await _channel .invokeMethod<bool?>('init') .then((value) => value ?? false); _wasInitialized = true; return _isAvailable; } return _isAvailable; } /// Вызов нативного окна SbpPay выбора банков. static Future<void> showPaymentModal(String link) { return _channel.invokeMethod('showPaymentModal', link); } }
Готово! Осталось написать простенькую реализацию в папке example, чтобы протестировать виджет:
sbp_pay/example/lib/main.dart
Формат ссылок, поддерживаемых плагином, описаны в API СБП:
main.dart
import 'package:flutter/material.dart'; import 'package:sbp_pay/sbp_pay.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); // Только ссылки такого формата позволяют открыть модалку, иначе падает исключение static const _paymentLink = 'https://qr.nspk.ru/AS100001ORTF4GAF80KPJ53K186D9A3G?type=01&bank=100000000007&crc=0C8A'; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Plugin example app'), ), body: Builder( builder: (context) { return Center( child: TextButton( onPressed: () async { final messenger = ScaffoldMessenger.of(context); if (await SbpPay.init()) { await SbpPay.showPaymentModal(_paymentLink); } else { messenger.showSnackBar( const SnackBar(content: Text('Not supported')), ); } }, child: const Text('Running on'), ), ); } ), ), ); } }
Результат
Примерно так будет выглядеть плагин СБП в вашем Flutter-приложении:

Получилась инструкция по написанию собственного Flutter-плагина для Android и iOS со всеми нюансами. Исходный код проекта доступен на GitHub. Версия 1.2 размещена в main ветке, а версия 1.4 — в ветке v1.4.
Пишите в комментариях, как вы решали данную проблему, делитесь отзывами и опытом.
ссылка на оригинал статьи https://habr.com/ru/articles/768610/
Добавить комментарий