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.processCardPaymentPayAppBridgeModule.startPayment(kopecks, inn) → Kotlin-бридж → dev.createEcrTerminalterminal.startPaymentTransaction.

Что удалось (коммит feat(pos): wire up PayApp SDK runtime deps + suspend init)

  1. Runtime-зависимости SDK закрыты (7 итераций NoClassDefFoundError на K10 Android 13):
    • retrofit2 + converter-gson + converter-scalars
    • okhttp3 + logging-interceptor
    • gson, timber, kotlin-logging-jvm, slf4j-api
    • kotlinx-serialization-json
    • ktor-client-core/cio/content-negotiation/serialization-kotlinx-json/logging
  2. 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.
  3. Suspend initialize(): PayAppManager.initialize() ждёт реального InitState.Initialized через Flow вместо немедленного return.
  4. Автозапрос разрешений при переходе в 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-экранами:

  1. Permissions UI (объяснения, ссылки на System Settings для MANAGE_EXTERNAL_STORAGE и т.п.).
  2. Activation — терминал регистрируется у PBF backend (cpay.pbfgroup.ru). Без активации appController остаётся uninitialized (мы это получали как UninitializedPropertyAccessException: lateinit property appController has not been initialized).
  3. 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):

  1. Наш POS формирует Intent с суммой и параметрами.
  2. Запускает ru.pbfgroup.payapp через startActivityForResult.
  3. PayApp сам проводит оплату (со своим UI, активацией и т.п.).
  4. Возвращает результат (approved/declined + RRN + slip) в onActivityResult.
  5. Наш 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. Нужно решение по направлению.

Ссылки