
Всем привет! Это Мурат Насиров и Артем Баркалов, мы Flutter-разработчики в Friflex. Разрабатываем высоконагруженные мобильные приложения для бизнеса и специализируемся на Flutter. В этой статье мы собрали большую часть кейсов, с которыми вы можете столкнуться при интеграции кнопки оплаты SberPay в приложении на Flutter. Это поможет вам понять механику работы СберПэй и шаги, которые необходимо сделать для передачи событий из натива во Flutter.
Содержание
-
3.1.1 Интеграция через Maven репозиторий
3.1.2 Интеграция вручную
3.1.3 Настройка проекта
3.1.4 Android-плагин и нативная кнопка
3.2.1 iOS плагин
Подготовка
Использование SberPay SDK в продакшене требует наличия установленного приложения «Сбербанк Онлайн» (СБОЛ), иначе в процессе оплаты произойдет ошибка. Тестирование можно провести либо на симуляторе iOS, либо на реальном устройстве iOS или Android. На эмуляторе Android тестирование не выйдет — при попытке оплаты возникает ошибка. Разработчики SDK используют аппаратные возможности смартфонов: геолокацию, bluetooth, Wi-Fi для снижения вероятности совершения мошеннических операций (условный антифрод).
Первым делом разработчик узнает от Сбербанка (по договору о подключении SberPay SDK в приложение), логин/пароль (credentials) и ссылку, которые указываются в файле build.gradle проекта (приложения) в части Android для получения библиотеки. Либо отдельно запрашивается aar-бандл. Для подключения SDK на iOS выдадут отдельную ссылку на репозиторий, хотя он публичный.
Вместе с данными для получения SDK выдают тестовые данные для регистрации заказа в шлюзе Сбера, а также специальные apiKey и merchantLogin, которые используются для инициализации SberPay SDK в вашем приложении. Тестовые данные не подходят для проверки списания реальных денежных средств с реальной карты, они исключительно для тестирования. Для работы SberPay в проде клиент, в приложение которого интегрируется этот SDK, должен подписать договор с Сбербанком о получении этих данных. После чего, их можно использовать для совершения оплаты с реальной банковской карты.
Регистрация заказа в шлюзе Сбера
В документации описан процесс регистрации заказов в платежном шлюзе Сбера, откуда необходимо в случае успешного ответа получить значение ключа sbolBankInvoiceId. То есть при оформлении заказа бекенд вашего проекта должен отправить запрос с параметрами, которые указаны в документации, откуда затем получить sbolBankInvoiceId и передать его мобильному приложению. С помощью коллбэков, которые Сбер будет отсылать вашему бекенду, будет понятно, совершена оплата или нет.
Для тестовой регистрации заказа достаточно отправлять запросы через Postman (файл):
POST https://3dsec.sberbank.ru/payment/rest/register.do, где register.do — одностадийная оплата:
{ "userName": "testUserName", // логин ЛК Сбера, выдается по договору "password": "testPassword", // пароль ЛК Сбера, выдается по договору "orderNumber": "e2574f1785324f1592d9029cb05adbbd", // уникальный номер заказа "amount": 19900, // сумма к оплате в копейках "returnUrl": "sbersdk://spay", // диплинк на приложение, возвращает к СДК "jsonParams": { "app2app": true, // Если true, в ответе придет sbolBankInvoiceId "app.osType" : "android", // Тип ОС (можно всегда запрашивать с таким) "app.deepLink": "sbersdk://spay" // диплинк на приложение, возвращает к СДК } }
Пример ответа:
{ "orderId": "1a8fb4ab-fe19-7372-94b6-2deb29335df0", "formUrl": "https://secure-payment-gateway.ru/payment/merchants/sbersafe_sberid/payment_ru.html?mdOrder=1a8fb4ab-fe19-7372-94b6-2deb29335df0", "externalParams": { "sbolInactive": "false", "sbolBankInvoiceId": "72e48b040afb4483b0a8c13c77e7e6f2", "sbolDeepLink": "sberpay://invoicing/v2?bankInvoiceId=72e48b040afb4483b0a8c13c77e7e6f2&operationType=app2app" } }
Поля returnUrl и app.deeplink должны служить диплинком для перехода обратно в мобильное приложение после авторизации через Сбербанк (либо СБОЛ), однако по факту они нигде не используются. Тем не менее, лучше их указывать в таком формате и обязательно сообщать в беседе с разработчиками SDK, что будет использоваться диплинк формата <вашасхема>://spay. Его нужно зарегистрировать на бекенде Сбера. Дальше он пригодится в реализации iOS-части плагина.
Маленький нюанс, при попытке зарегистрировать заказ, все символы валидного sbolBankInvoiceId должны быть в нижнем регистре. Если по каким-то причинам во время оплаты через модальное окно СберПэя произошла именно ошибка (не отмена), тогда sbolBankInvoiceId становится использованным, и зарегистрировать заказ с таким же orderNumber не получится. Одним из решений может быть перевод заказа в статус ожидания оплаты на бекенде. После чего можно оплатить заказ другим способом: например, через эквайринг. На iOS и Android по-разному реализована реакция на отмену оплаты, об этом — подробности ниже.
Создание плагина
Для создания плагина на Flutter используется команда (документация):
flutter create --org plugin.sdk --template=plugin --platforms=android,ios sber_pay
Где:
-
plugin.sdk— путь до исполняемого файла плагина на Android-стороне; -
--template=plugin— указание, что создаётся именно плагин; -
sber_pay— название плагина.
Если вы решитесь отправить свой проект в удаленный репозиторий, следует обязательно сделать его приватным — ввиду наличия в коде секретных данных.
Внедрение Android-части
Минимальная версия Android SDK — 21.
Есть 2 пути интеграции SDK.
1) Интеграция через Maven репозиторий
В файле android/build.gradle, внутри android {...}, нужно найти dependencies и указать:
dependencies { implementation( 'ru.spaymentsplus.libraries:spaysdk:1.2.4' ) { transitive = true } }
По договору вам передадут секретные данные для получения aar-бандла. Вообще, как сказали разработчики SDK, если вы планируете отправлять свое приложение в удаленный репозиторий, то достаточно сделать его приватным и давать доступ только команде. Если же вы работаете один, можно рассмотреть такой способ.
Создаем файл sber_pay/example/android/sberpay.properties и указываем переменные:
sPayUrl=ссылка из договора для получения бандла sPayUsername=логин из договора для получения бандла sPayPassword=пароль из договора для получения бандла
В файле sber_pay/example/android/.gitignore указываем sberpay.properties как игнорируемый, чтобы он не попал в удаленный репозиторий.
Теперь переходим в build.gradle и получаем aar-бандлы по указанным в sberpay.properties данным:
def sberpayProperties = new File('sberpay.properties') def properties = new Properties() properties.load(sberpayProperties.newDataInputStream()) def link = properties.getProperty('sPayUrl') def login = properties.getProperty('sPayUsername') def pass = properties.getProperty('sPayPassword') allprojects { repositories { google() mavenCentral() maven { name = "GitHubPackages" url = uri(link) credentials { username = login password = pass } } } }
Таким образом удастся получить библиотеки и при этом не раскрыть секретные логин/пароль/ссылку. Насколько удалось выяснить, получение бандлов таким способом вызвано тем, что SberPay SDK использует специальную библиотеку для профилирования, у которой есть требования к безопасности. Из-за этого этот SDK (по крайней мере на Android стороне) не может быть публичным.
2) Интеграция вручную
Получив aar-бандлы, создаем в папке android папку libs и перемещаем туда библиотеки. Затем в build.gradle подключаются эти библиотеки вместе с транзитивными зависимостями. Внутри android {...} нужно найти dependencies и указать
dependencies { // .aar бандлы SberPay SDK implementation files('../libs/bms-sdk-fingerprint_VERSION_release.aar') implementation files('../libs/SDK-VERSION.aar') // Транзитивные библиотеки, необходимые для работы SberPay SDK implementation 'com.google.android.material:material:<version>' implementation 'io.github.sberid:SberIdSDK:<version>' implementation 'com.google.dagger:dagger:<version>' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:<version>' }
Подключаемые здесь aar-бандлы, если отправляются в контроль версий, должны храниться только в приватном репозитории.
Настройка проекта
После получения SberPay SDK нужно установить minSdkVersion 21 в sber_pay/android/build.gradle:
defaultConfig { minSdkVersion 21 }
Проверьте, что в sber_pay/example/android/app/build.gradle также указана 21 версия:
defaultConfig { … minSdkVersion 21 }
Еще в приложении стоит добавить разрешения для доступа к геолокации в файле sber_pay/example/android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_UPDATES" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Это увеличит шансы для совершения успешной оплаты.
Для работы с сервисом SberPay в релизе, если у вас включена обфускация необходимо добавить proguard. В android/app/build.gradle по умолчанию shrinkResources и minifyEnabled не указаны:
android { ... buildTypes { release { // shrinkResources и minifyEnabled не указаны, по-умолчанию true, однако лучше указать явно shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } }
Теперь в android/app/proguard-rules.pro добавляем:
-keep class spay.sdk.** { *; } -keepclassmembers,allowobfuscation class * { @com.google.gson.annotations.SerializedName <fields>; }
Если вам не нужна обфускация, тогда shrinkResources и minifyEnabled в android/app/build.gradle должны быть false:
android { ... buildTypes { release { shrinkResources false minifyEnabled false } } }
Соответственно, создавать android/app/proguard-rules.pro в таком случае не нужно.
Android-плагин и нативная кнопка
Когда с получением библиотеки разобрались, можно приступать к внедрению нативной кнопки СберПэя. Как комментировали сами разработчики этого SDK, решение завязывать весь функционал оплаты на кнопке предстоит пересмотреть в будущих версиях.
В документации к СберПэю предлагают создать специальный PlatformView, который затем разворачивается на стороне Flutter-приложения. Делать это необязательно, в Flutter-части этот процесс будет описан подробнее.
Пока что настроим файл плагина sber_pay/android/src/main/kotlin/plugin/sdk/sber_pay/SberPayPlugin.kt:
Настройка SberPayPlugin.kt
/** * Плагин для оплаты с использованием SberPay. Для работы нужен установленный Сбербанк (либо Сбол). */ class SberPayPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel private lateinit var binding: FlutterPluginBinding private lateinit var activity: Activity private lateinit var context: Context /** Кнопка для управления оплатой **/ private lateinit var button: SPayButton override fun onAttachedToEngine(flutterPluginBinding: FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "sber_pay") binding = flutterPluginBinding context = flutterPluginBinding.applicationContext channel.setMethodCallHandler(this) } override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { // Инициализация "init" -> { initialize(call, result) } // Проверка готовности к оплате "isReadyForSPaySdk" -> { /** * Метод для проверки готовности к оплате. * Зависит от переданного аргумента [env] при инициализации через метод [initialize] * (см. комментарий к методу). Запрос может выполняться долго. * * @return Если у пользователя нет установленного сбера в режимах * SPayStage.SandboxRealBankApp, SPayStage.prod - вернет false. */ result.success(button.isReadyForSPaySdk()) } // Оплата "payWithBankInvoiceId" -> { showSberPaymentModal(call, result) } else -> { result.notImplemented() } } } /** * Метод для оплаты, в аргументы которого обязательно необходимо передать: * @property apiKey ключ, выдаваемый по договору, либо создаваемый в личном кабинете; * @property merchantLogin логин, выдаваемый по договору, либо создаваемый в личном кабинете; * @property appPackage пакет вашего приложения; * @property language использовано по умолчанию "RU"; * @property bankInvoiceId параметр, который получаем после запроса для регистрации заказа в * шлюзе Сбера. */ private fun showSberPaymentModal(call: MethodCall, result: Result) { val args = call.arguments as Map<*, *> var responseSent = false // Флаг для отслеживания отправки ответа var hasError = false // Флаг для отслеживания отправки ошибки try { val apiKey = args["apiKey"] as String val merchantLogin = args["merchantLogin"] as String val bankInvoiceId = args["bankInvoiceId"] as String val appPackage = context.packageName val language = "RU" if (!responseSent) { button.payWithBankInvoiceId(apiKey, merchantLogin, bankInvoiceId, appPackage, language) { response -> when (response) { // Оплата не завершена is PaymentResult.Processing -> result.success("processing") // Оплата прошла успешно is PaymentResult.Success -> result.success("success") // Оплата прошла с ошибкой is PaymentResult.Error -> { if (!hasError) { hasError = true if (response.merchantError is MerchantError.SdkClosedByUser) { result.success("cancel") return@payWithBankInvoiceId } result.error("-", "MerchantError", response.merchantError?.description ?: "Ошибка выполнения оплаты") return@payWithBankInvoiceId } } } responseSent = true } } } catch (error: Exception) { result.error("-", error.localizedMessage, error.message) } } /** * Метод инициализации, выполняется перед стартом приложения. * [env], полученный из FLutter, Тесты со всеми типами [env] лучше всего проводить на реальном * устройстве. Он определяет тип запуска: * * @property SPayStage.SandboxRealBankApp устройство с установленным Сбером; * @property SPayStage.SandBoxWithoutBankApp устройство без Сбера; * @property SPayStage.prod устройство с установленным Сбером, работает с продовыми данными. */ private fun initialize(call: MethodCall, result: Result) { val args = call.arguments as Map<*, *> val sPayStage = when (args["env"] as String) { "sandboxRealBankApp" -> SPayStage.SandboxRealBankApp "sandboxWithoutBankApp" -> SPayStage.SandBoxWithoutBankApp else -> { SPayStage.Prod } } // Оплата частями val enableBnpl = args["enableBnpl"] as Boolean? ?: false try { SPaySdkApp.getInstance().initialize(application = activity.application, stage = sPayStage, enableBnpl = enableBnpl) result.success(true) } catch (e: Exception) { result.error("-", e.localizedMessage, e.message) } } override fun onDetachedFromEngine(binding: FlutterPluginBinding) { channel.setMethodCallHandler(null) } override fun onAttachedToActivity(activityBinding: ActivityPluginBinding) { activity = activityBinding.activity button = SPayButton(activity, null) } override fun onReattachedToActivityForConfigChanges(activityBinding: ActivityPluginBinding) { activity = activityBinding.activity button = SPayButton(activity, null) } override fun onDetachedFromActivity() {} override fun onDetachedFromActivityForConfigChanges() {} }
Внедрение iOS-части
Минимальная версия iOS — 12.0, XCode 13
Начнем с подключения SDK. Для добавления нативного SDK необходимо перенести папку SPaySdk.xcframework в папку iOS-плагина.
Для работы с плагином в приложении, в которое он будет интегрирован, необходимо добавить следующие параметры в файл sber_pay/example/ios/Runner/info.plist:
<key>DTXAutoStart</key> <string>false</string> <key>LSApplicationQueriesSchemes</key> <array> <string>sbolidexternallogin</string> <string>sberbankidexternallogin</string> </array> <key>NSAppTransportSecurity</key> <dict> <key>NSExceptionDomains</key> <dict> <key>gate1.spaymentsplus.ru</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> <key>ift.gate2.spaymentsplus.ru</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> <key>cms-res.online.sberbank.ru</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> </dict> </dict> <key>NSBluetoothAlwaysUsageDescription</key> <string>Данные Bluetooth собираются и отправляются на сервер для безопасного проведения оплаты</string> <key>NSBluetoothPeripheralUsageDescription</key> <string>Данные Bluetooth собираются и отправляются на сервер для безопасного проведения оплаты</string> <key>NSLocationWhenInUseUsageDescription</key> <string>Данные локации необходимы для безопасного проведения оплаты</string>
В отличие от Android, нужно добавить параметр для настройки диплинка на наше приложение, с помощью которого будет осуществлен возврат в наше приложение после перехода в Сбербанк онлайн/СБОЛ.
В Android данный функционал реализуется передачей параметра package. Если приложение до этого уже было настроено на работу с диплинками, можно использовать имеющуюся схему. Далее в реализации плагина будет дополнительно указана проверка на host в диплинке. В нашем случае переход обратно в приложение из приложения Сбербанка/СБОЛа будет по диплинку sbersdk://spay:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLName</key> <string>spay</string> <key>CFBundleURLSchemes</key> <array> <string>sbersdk</string> </array> </dict> </array>
Также можно добавить через вкладку Info→URL Types:
Далее необходимо добавить в Capabilities проекта Access wi-fi information. Для этого необходимо выбрать: Ваш таргет → Signing & Capabilities → +Capability → Access wi-fi information.
В части плагина, в sber_pay/ios/sber_pay.podspec необходимо добавить код:
s.preserve_paths = 'SPaySdk.xcframework/**/*' s.xcconfig = { 'OTHER_LDFLAGS' => '-framework SPaySdk' } s.vendored_frameworks = 'SPaySdk.xcframework'
В это же файле нужно установить iOS 12 версии:
s.platform = :ios, '12.0'
Соответственно на стороне приложения тоже должна быть iOS 12. Установить ее можно выбрав в таргете нужную версию в настройках Minimum deployments.
iOS плагин
Нативная реализация SDK на платформе iOS не привязана к кнопке, поэтому сразу приступим к реализации плагина. Настроим sber_pay/ios/Classes/SberPayPlugin.swift. Аналогично платформе Android можно не создавать нативную кнопку, а использовать только готовое API.
Настройка SberPayPlugin.swift
// Плагин для оплаты с использованием SberPay. public class SberPayPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "sber_pay", binaryMessenger: registrar.messenger()) let instance = SberPayPlugin() registrar.addMethodCallDelegate(instance, channel: channel) /// Создание [addApplicationDelegate] для перехода по диплинку обратно в приложение registrar.addApplicationDelegate(instance) } public func application(_ app: UIApplication,open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { /// Если при открытии приложения с диплинком если он содержит хост "spay", то такой диплинк /// попадает в нативный плагин. Таким образом работает возврат в приложение и получение данных нативным /// SDK от приложения Сбербанк онлайн/СБОЛ if url.host == "spay" { SPay.getAuthURL(url) } return true } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { /// Инициализация case "init": initialize(call, result:result) /// Проверка готовности к оплате case "isReadyForSPaySdk": /** Метод для проверки готовности к оплате. Зависит от переданного аргумента [env] при инициализации через метод [initialize] (см. комментарий к методу). Запрос может выполняться долго. - Returns Если у пользователя нет установленного сбера в режимах SEnvironment.sandboxRealBankApp, SEnvironment.prod - вернет false. */ result(SPay.isReadyForSPay) // Оплата case "payWithBankInvoiceId": payWithBankInvoiceId(call, result: result) default: result(FlutterMethodNotImplemented) } } /** Метод для оплаты, в аргументы которого обязательно необходимо передать: - Parameter apiKey ключ, выдаваемый по договору, либо создаваемый в личном кабинете; - Parameter merchantLogin логин, выдаваемый по договору, либо создаваемый в личном кабинете; - Parameter bankInvoiceId параметр, который получаем после запроса для регистрации заказа в шлюзе Сбера. - Parameter redirectUri диплинк обратно в приложение после перехода в Сбербанк */ private func payWithBankInvoiceId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let args = call.arguments as? [String: Any], let apiKey = args["apiKey"] as? String, let merchantLogin = args["merchantLogin"] as? String, let bankInvoiceId = args["bankInvoiceId"] as? String, let redirectUri = args["redirectUri"] as? String else { result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments", details: nil)) return } if bankInvoiceId.count != 32 { result(FlutterError(code: "-", message: "MerchantError", details: "Длина bankInvoiceId должна быть 32 символа")) return } guard let topController = getTopViewController() else { result(FlutterError(code: "PluginError", message: "SberPay: Failed to implement controller", details: nil)) return } let request = SBankInvoicePaymentRequest( merchantLogin: merchantLogin, bankInvoiceId: bankInvoiceId, language: "RU", redirectUri: redirectUri, apiKey: apiKey) SPay.payWithBankInvoiceId(with: topController, paymentRequest: request) { state, info in switch state { case .success: result("success") case .waiting: result("processing") case .cancel: result("cancel") case .error: result(FlutterError(code: "-", message: "Ошибка оплаты", details: info)) @unknown default: result(FlutterError(code: "-", message: "Неопределенная ошибка", details: info)) } } } /** Метод инициализации, выполняется перед стартом приложения. [env], полученный из FLutter, Тесты со всеми типами [env] лучше всего проводить на реальном устройстве. Он определяет тип запуска: - Parameter SEnvironment.sandboxRealBankApp устройство с установленным Сбером; - Parameter SEnvironment.sandboxWithoutBankApp устройство без Сбера; - Parameter SEnvironment.prod устройство с установленным Сбером, работает с продовыми данными. - Parameter enableBnpl Функционал Оплата частями */ private func initialize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let args = call.arguments as? [String: Any], let env = args["env"] as? String else { result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments", details: nil)) return } let enableBnpl = args["enableBnpl"] as? Bool ?? false let sPayStage: SEnvironment switch env { case "sandboxRealBankApp": sPayStage = .sandboxRealBankApp case "sandboxWithoutBankApp": sPayStage = .sandboxWithoutBankApp default: sPayStage = .prod } SPay.setup(bnplPlan: enableBnpl, environment: sPayStage) result(true) } private func getTopViewController() -> UIViewController? { var topController = UIApplication.shared.keyWindow?.rootViewController while let presentedViewController = topController?.presentedViewController { topController = presentedViewController } return topController } }
Внедрение платформ в Flutter-приложение
Наконец можно приступать к написанию кода в Flutter-части. Ранее упоминалось, что можно использовать нативную кнопку оплаты из SberPay SDK через PlatformView. Однако лучше всего просто сверстать ее в Flutter. В случае чего ее можно будет легко отредактировать.
Обратите внимание, здесь мы создаем кнопку именно на стороне плагина, делается это потому, что нам нужно соответствовать гайдланам Сбера по дизайну и исключить редактирование кнопки. Тем не менее, фактически от нее не зависит функционал оплаты, что является плюсом. Поэтому сначала в корне плагина создаем папку assets, в которой добавим лого СберПэя и шрифты (их можно получить из гайдбука Сбера).
После чего в pubspec.yaml объявим созданные ассеты:
flutter: assets: - assets/sberpay_logo.png fonts: - family: SberPaySans fonts: - asset: assets/fonts/spay_sans_text.ttf
Теперь создадим кнопку СберПэя в папке lib плагина:
/// Виджет нативной кнопки оплаты Сбербанка class SberPayButton extends StatelessWidget { const SberPayButton({ super.key, this.onPressed, }); /// Обработчик нажатия на кнопку final VoidCallback? onPressed; @override Widget build(BuildContext context) { return InkWell( onTap: onPressed, child: Container( height: 48, decoration: BoxDecoration( color: const Color(0xFF21A038), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Flexible( child: Text( 'Оплатить', style: TextStyle( color: Colors.white, fontSize: 16, fontFamily: 'SberPaySans', package: 'sber_pay', ), ), ), const SizedBox(width: 8), Image.asset( 'assets/sberpay_logo.png', package: 'sber_pay', width: 48, height: 22, ), ], ), ), ); } }
СберПэй позволяет использовать свой SDK в трех режимах:
-
prod— прод; -
sandboxWithRealBank— песочница с банковским приложением; -
sandboxWithoutBank— песочница.
Последние два режима применяются для тестирования процесса оплаты. Они нужны, чтобы предупредить возможные сложности с использованием СберПэя. Для каждого из режимов создадим enum, который будет определять режим запуска SberPay SDK:
/// Тип инициализации сервисов Сбербанка enum SberPayEnv { /// Продуктовый режим. /// /// Для авторизации пользователя происходит редирект в приложение Сбербанка. prod, /// Режим песочницы. /// /// Позволяет протестировать оплату как в [prod], но с тестовыми данными. /// Для авторизации пользователя происходит редирект в приложение Сбербанка. /// /// На Android тестируется только на реальных устройствах. На iOS можно /// тестировать как на реальном устройстве, так и в симуляторе. sandboxRealBankApp, /// Режим песочницы без перехода в банк. /// /// При авторизации пользователя не осуществляется переход в приложение /// Сбербанка. /// /// На Android тестируется только на реальных устройствах. На iOS можно /// тестировать как на реальном устройстве, так и в симуляторе. sandboxWithoutBankApp }
Добавим еще один специальный enum, который будет транслировать платформенные ответы в удобный вид:
/// Статусы оплаты enum SberPayPaymentStatus { /// Успешный результат success, /// Необходимо проверить статус оплаты processing, /// Пользователь отменил оплату cancel, /// Неизвестный тип unknown; static fromString(String? value) { switch (value) { case "success": return SberPayPaymentStatus.success; case "processing": return SberPayPaymentStatus.processing; case "cancel": return SberPayPaymentStatus.cancel; default: return SberPayPaymentStatus.unknown; } } }
Когда все это выполнено, можно приступать к написанию логики плагина. Flutter с новых версий использует немного другой подход в общении между платформами и фреймворком. В нашем случае будет применяться более упрощенный старый подход. Поэтому все сгенерированные платформенные файлы можно удалить, оставив лишь главный файл плагина.
sber_pay.dart
/// Плагин для отображения нативной кнопки SberPay SDK /// /// Все исключения (Exceptions) приходящие из методов этого класса должны /// обрабатываться уровнем выше. class SberPayPlugin { static const methodChannel = MethodChannel('sber_pay'); /// Инициализация SberPay SDK. /// /// Необходимо выполнить для начала работы с библиотекой. /// На платформе Android этот метод является асинхронным, однако у /// него нет API (коллбека) для выполнения кода после завершения /// инициализации. /// /// * [env] - среда запуска, которая определяется через [SberPayEnv]. /// * [enableBnpl] - функционал оплаты частями static Future<bool> initSberPay({ required String env, bool? enableBnpl, }) async { final result = await methodChannel.invokeMethod<bool>('init', { 'env': env, 'enableBnpl': enableBnpl, }); return result ?? false; } /// Метод для проверки готовности к оплате. /// /// Зависит от переданного аргумента [env] при инициализации через метод /// [initSberPay] (см. комментарий к методу). /// /// Запрос может выполняться долго, поэтому здесь стоит искусственная /// задержка, чтобы дождаться инициализации SDK. /// /// Если у пользователя нет установленного сбера в режимах /// [SberPayEnv.sandboxRealBankApp], [SberPayEnv.prod] - вернет false. static Future<bool> isReadyForSPaySdk() async { final result = await methodChannel.invokeMethod<bool>('isReadyForSPaySdk'); if (result == null || result == false) { await Future.delayed( const Duration(seconds: 2), () async { return await methodChannel.invokeMethod<bool>('isReadyForSPaySdk'); }, ); } return result ?? false; } /// Метод оплаты через SberPay SDK. /// * [apiKey] - ключ, выдаваемый по договору, либо создаваемый в личном /// кабинете; /// * [merchantLogin] - логин, выдаваемый по договору, либо создаваемый в /// личном кабинете; /// * [bankInvoiceId] - параметр, который получаем после запроса для /// регистрации заказа в шлюзе Сбера. /// * [redirectUri] - диплинк для перехода обратно в приложение после открытия /// Сбербанка (только на iOS). /// /// Возвращает статус оплаты [SberPayPaymentStatus] static Future<SberPayPaymentStatus> payWithBankInvoiceId({ required String apiKey, required String merchantLogin, required String bankInvoiceId, required String redirectUri, }) async { final result = await methodChannel.invokeMethod<String>( 'payWithBankInvoiceId', { 'apiKey': apiKey, 'merchantLogin': merchantLogin, 'bankInvoiceId': bankInvoiceId, 'redirectUri': redirectUri, }, ); return SberPayPaymentStatus.fromString(result); } }
В методе isReadyForSPaySdk стоит искусственная задержка, потому что метод initialized на стороне Android выполняется асинхронно, и нельзя дождаться его выполнения, так как в этом методе нет соответствующего API (например, коллбэка при его завершении). На iOS такой проблемы нет. Разработчики SDK обещали исправить это в будущих версиях.
Плагин полностью готов к работе. Теперь можно создать реализацию в sber_pay/example/lib/main.dart для тестирования SberPay SDK. Она может быть простенькая, а может быть как эта:
main.dart
/// Необходимо указать по данным из договора const _apiKey = ''; const _merchantLogin = ''; /// Диплинк на переход в приложение const _redirectUri = 'sbersdk://spay'; void main() => runApp(const SberPayExampleApp()); class SberPayExampleApp extends StatefulWidget { const SberPayExampleApp({super.key}); @override State<SberPayExampleApp> createState() => _SberPayExampleAppState(); } class _SberPayExampleAppState extends State<SberPayExampleApp> { late final TextEditingController _controller; late String _paymentStatus; late bool _isPluginLoading; late bool _isAppReadyForPay; late bool _isPluginInitialized; late SberPayEnv _selectedInitType; late Color _color; @override void initState() { super.initState(); _controller = TextEditingController(); _paymentStatus = ''; _selectedInitType = SberPayEnv.sandboxWithoutBankApp; _isPluginLoading = false; _isAppReadyForPay = false; _isPluginInitialized = false; _color = Colors.grey; _readyForPay(); } Future<void> _readyForPay() async { setState(() => _isPluginLoading = true); _isPluginInitialized = await SberPayPlugin.initSberPay( env: _selectedInitType.name, ); if (mounted) setState(() {}); _isAppReadyForPay = await SberPayPlugin.isReadyForSPaySdk(); if (!_isAppReadyForPay) { _isAppReadyForPay = await SberPayPlugin.isReadyForSPaySdk(); } if (mounted) setState(() => _isPluginLoading = false); } void _setEnv(SberPayEnv env) { _selectedInitType = env; _paymentStatus = ''; _color = Colors.grey; _readyForPay(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF21A038)), useMaterial3: true, ), home: Scaffold( appBar: AppBar( title: const Text('SberPay plugin example'), backgroundColor: const Color(0xFF21A038), centerTitle: true, ), body: Builder( builder: (context) { return Padding( padding: const EdgeInsets.all(8.0), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Table( border: TableBorder.all(), defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ _tableRowWrapper( 'Тип запуска', Column( children: [ ChoiceChip( label: const Text('Прод'), selected: _selectedInitType == SberPayEnv.prod, onSelected: (_) => _setEnv(SberPayEnv.prod), ), const SizedBox( width: 10, ), ChoiceChip( label: const Text('Песочница/Банк'), selected: _selectedInitType == SberPayEnv.sandboxRealBankApp, onSelected: (_) => _setEnv( SberPayEnv.sandboxRealBankApp, ), ), const SizedBox( width: 10, ), ChoiceChip( label: const Text('Песочница'), selected: _selectedInitType == SberPayEnv.sandboxWithoutBankApp, onSelected: (_) => _setEnv( SberPayEnv.sandboxWithoutBankApp, ), ), ], ), ), _tableRowWrapper( 'Плагин проинициализирован', Text(_isPluginLoading ? 'Загрузка' : _isPluginInitialized ? "ДА" : "НЕТ"), ), _tableRowWrapper( 'Оплата доступна', Text(_isPluginLoading ? 'Загрузка' : _isAppReadyForPay ? "ДА" : "НЕТ"), ), _tableRowWrapper( 'Статус операции оплаты', SizedBox( height: 80, child: Row( children: [ CircleAvatar( backgroundColor: _color, radius: 10.0, ), Flexible( child: Text( _paymentStatus.isEmpty ? "Оплата не производилась" : _paymentStatus, textAlign: TextAlign.center, ), ) ], ), ), ), ]), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( controller: _controller, textInputAction: TextInputAction.done, decoration: InputDecoration( hintText: 'Введите bankInvoiceID', suffixIcon: GestureDetector( onTap: () { _controller.clear(); _color = Colors.grey; setState(() => _paymentStatus = ''); }, child: const Icon(Icons.close, size: 32), ), ), ), ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _isPluginLoading ? Container( height: 48, decoration: BoxDecoration( color: const Color(0xFF21A038), borderRadius: BorderRadius.circular(12), ), child: const Center( child: CircularProgressIndicator( color: Colors.white, ), ), ) : SberPayButton( onPressed: () async { if (_apiKey.isEmpty || _merchantLogin.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Не заданы apiKey и/или merchantLogin', ), duration: Duration(seconds: 2), ), ); return; } if (_controller.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Введите bankInvoiceID'), duration: Duration(seconds: 2), ), ); } else { try { final result = await SberPayPlugin .payWithBankInvoiceId( apiKey: _apiKey, merchantLogin: _merchantLogin, bankInvoiceId: _controller.text, redirectUri: _redirectUri, ); switch (result) { case SberPayPaymentStatus.success: _color = Colors.green; _paymentStatus = 'Оплата прошла успешно'; break; case SberPayPaymentStatus.processing: _color = Colors.yellow; _paymentStatus = 'Необходимо проверить статус оплаты'; break; case SberPayPaymentStatus.cancel: _color = Colors.blue; _paymentStatus = 'Пользователь отменил оплату'; break; case SberPayPaymentStatus.unknown: _color = Colors.purple; _paymentStatus = 'Неизвестное состояние'; } setState(() {}); } on PlatformException catch (e) { setState( () { _color = Colors.red; _paymentStatus = e.details ?? ''; }, ); } } }, ), ), ], ), ), ); }, ), ), ); } TableRow _tableRowWrapper(String title, Widget secondChild) { return TableRow( children: [ TableCell(child: Text(title, textAlign: TextAlign.center)), Padding( padding: const EdgeInsets.all(4.0), child: Center(child: secondChild), ), ], ); } }
Стоит учитывать, что это пример использования, и он не должен быть в продакшен коде 🙂
Для совершения оплаты в методе payWithBankInvoiceId указываются apiKey и merchantLogin, которые должны быть приватными. Конечно, их точно так же, как и credentials, для получения aar-бандла можно сразу указать здесь, и даже если отправлять в контроль версий, то только в приватный. Все же лучше получать эти данные из переменных среды, которые могут быть предварительно обфусцированы. Это повысит вероятность сохранения важных данных.
Также в этом методе, в аргументе redirectUri, указывается диплинк, который до этого мы регистрировали в iOS части приложения. Он понадобится для перехода из вашего приложения в приложение Сбербанка и обратно.
На Android и iOS по-разному реализована отмена оплаты (событие, когда пользователь свернул нативное модальное окно либо когда нажал «Отменить оплату»). Если в iOS приходит отдельное событие cancel, то в Android это приходит как ошибка.
После всех вышеизложенных трудов получается что-то такое:


Статья получилась очень насыщенной всякими нюансами. Когда смотришь на готовый результат, кажется, что ничего особенного в реализации нет. Однако все проекты создают люди, где-то могут быть ошибки, где-то могут быть особенности проектирования и многое другое. Поэтому получается то, что получается. И наверное, это нормально. Важно лишь поддерживать всегда стремление совершенствовать свои продукты и проекты, ведь из этого в целом и состоит разработка.
Финальный проект мы разместили в GitHub. Как обычно, делитесь своим мнением и наработками в данной области под этим постом.
ссылка на оригинал статьи https://habr.com/ru/articles/771354/
Добавить комментарий