ADR-014 — PayApp SDK headless тупик на K10
Контекст
Цель: встроить PayApp SDK (PBF Group, ru.pbfgroup.payapp.core) в erp-pos мобильное приложение (Expo 52 / RN 0.76) как нативный Kotlin-бридж, чтобы POS-касса принимала оплату картой напрямую через встроенный терминал Centerm K10, без запуска стороннего приложения.
Текущая архитектура: mobile/modules/payapp-bridge/ — Expo Native Module обёртка над SDK. PayAppPaymentClient.processCardPayment → PayAppBridgeModule.startPayment(kopecks, inn) → Kotlin-бридж → dev.createEcrTerminal → terminal.startPaymentTransaction.
Что удалось (коммит feat(pos): wire up PayApp SDK runtime deps + suspend init)
- Runtime-зависимости SDK закрыты (7 итераций NoClassDefFoundError на K10 Android 13):
retrofit2+ converter-gson + converter-scalarsokhttp3+ logging-interceptorgson,timber,kotlin-logging-jvm,slf4j-apikotlinx-serialization-jsonktor-client-core/cio/content-negotiation/serialization-kotlinx-json/logging
- Permissions launcher зарегистрирован в
MainActivity.onCreateдоsuper.onCreate, черезdev.preparePermissions(activity). Все runtime-permissions (Camera, ReadPhoneState, Storage, Location, Bluetooth, etc.) granted черезpm grantиappops— включая MANAGE_EXTERNAL_STORAGE, WRITE_SETTINGS. - Suspend initialize():
PayAppManager.initialize()ждёт реальногоInitState.Initializedчерез Flow вместо немедленного return. - Автозапрос разрешений при переходе в
InitState.RequestPermissions: бридж автоматически вызываетdev.requestPermissions(activity)на Main-диспатчере.
Где застряли
SDK проходит последовательно Idle → Loading → RequestPermissions и никогда не движется дальше:
initState → Idle
initState → Loading
initState → RequestPermissions
[dev.requestPermissions(activity) вызван]
... ничего, никаких новых состояний
Наблюдения на реальном K10 (device fbd4722b, Android 13, arm64-v8a, в Wi-Fi с доступом к cpay.pbfgroup.ru, ping 27ms):
- Системный диалог запроса разрешений не всплывает — потому что все нужные permissions уже granted (SDK декларирует только CAMERA, READ_PHONE_STATE в рефлексированных strings).
dev.requestPermissions(activity)не кидает исключения.- Callback
ActivityResultContracts.RequestMultiplePermissionsс пустой/all-granted картой должен сработать синхронно и перевести SDK в следующее состояние — но не переводит. InitState.PermissionsDeniedтоже не эмитится.- В логах SDK тишина, только наши логи бриджа.
До этого состояния была гонка инициализации: appController (lateinit в PbfPayAppDevice, инициализируется внутри suspend initDeviceController) ещё не выставлен, когда вызывался createEcrTerminal. Fix: сделали initialize() suspend и ждём Initialized. Проблема сместилась на уровень выше — до Initialized не доходим вообще.
Почему так
PayApp SDK поставляется в паре со standalone-приложением ru.pbfgroup.payapp (пакет установлен на K10, при запуске показывает splash «PBF Pay» и закрывается). Это приложение содержит полный onboarding + activation flow с собственными UI-экранами:
- Permissions UI (объяснения, ссылки на System Settings для MANAGE_EXTERNAL_STORAGE и т.п.).
- Activation — терминал регистрируется у PBF backend (
cpay.pbfgroup.ru). Без активацииappControllerостаётся uninitialized (мы это получали какUninitializedPropertyAccessException: lateinit property appController has not been initialized). - PIN/ключи/EMV-параметры (
EmvInitStateActionклассы видны в AAR).
HeadlessUiAdapter из API SDK только подавляет UI-роуты, но не заменяет логику онбординга. Публичный API SDK (PayAppDevice интерфейс: preparePermissions, requestPermissions, initialize, createEcrTerminal, getActivationRepository, getActivationController) предполагает, что хост-приложение рендерит свой UI поверх этих API — headless-сценарий недокументирован.
Варианты разблокировки
Вариант А — Intent-интеграция с standalone PayApp (рекомендуется)
Не встраивать SDK в наш APK. Вместо этого общаться с установленным ru.pbfgroup.payapp через Intent (классический ECR-pattern как iiko/evotor):
- Наш POS формирует Intent с суммой и параметрами.
- Запускает
ru.pbfgroup.payappчерезstartActivityForResult. - PayApp сам проводит оплату (со своим UI, активацией и т.п.).
- Возвращает результат (approved/declined + RRN + slip) в onActivityResult.
- Наш POS получает результат и продолжает flow.
Плюсы: онбординг и активация — забота PayApp, мы получаем готовый стандартный интерфейс. Весь текущий bridge (~200 строк Kotlin + 13 AAR/JAR) выбрасывается, APK уменьшается со 186 MB до ~50 MB.
Минусы: нужна от PBF схема Intent (extras, result codes, формат RRN-возврата). На момент ADR у нас этой документации нет.
Action items: запросить у PBF Group документацию по Intent-протоколу или ECR API для external POS.
Вариант B — Продолжить headless с полной поддержкой
Реализовать в бридже всё, что делает standalone PayApp:
- UI для
ActivationController(когда SDK требует активации — рендерить экран регистрации). - Обработку
MANAGE_EXTERNAL_STORAGEиWRITE_SETTINGSчерез Settings Intent, а не ActivityResultLauncher. - Поддержка всех
InitState.*подклассов, не толькоInitialized.
Минусы: это по сути переписывание PayApp UI; реалистично только при наличии прямой поддержки PBF.
Вариант C — StubPaymentClient для разработки
Для фронтенд/UX-итераций вернуть выбор StubPaymentClient (уже есть в коде, эмулирует route events и RRN). Реальные карты отложить до разрешения варианта А или B.
Статус
proposed. Нужно решение по направлению.
Ссылки
- Коммит erp-pos:
feat(pos): wire up PayApp SDK runtime deps + suspend init - Бизнес-спека PayApp SDK
- Репозитории