Обзор

Интеграция

Подключение @qor/call-screening к гибридному приложению — установка, онбординг разрешений, списки, деградация, тестирование.

Установка

В приложение (workspace монорепо):

pnpm add @qor/call-screening
npx cap sync

Плагин сам декларирует Android-сервис и разрешения — правки AndroidManifest.xml не нужны. Для iOS дополнительно требуется extension-таргет (см. ниже).

Требования: Capacitor ≥ 7, Android minSdk 26 (полный функционал с API 29), iOS 15+. Сборка Android — JDK 21 (JAVA_HOME должен указывать на него, иначе invalid source release: 21).

Nuxt-приложения (@qor/hybrid)

@qor/hybrid уже содержит всё для работы с плагином — достаточно, чтобы @qor/call-screening был в package.json приложения (optional peer).

Экран «Состояние защиты»

<script setup lang="ts">
function onConfigureLiveLookup() {
  // iOS 18+: приложение само знает URL своего PIR-сервиса
}
</script>

<template>
  <!-- Автоимпорт из @qor/hybrid: карта возможностей с кнопками восстановления,
       живое обновление по capabilityChanged -->
  <QorProtectionStatus @configure-live-lookup="onConfigureLiveLookup" />
  <QorScreeningLog :limit="50" />
</template>

Композабл

const screening = useCallScreening()

// Онбординг (например, в настройках приложения)
await screening.requestScreeningRole()      // Android: системный диалог роли; iOS: настройки блокировки
await screening.requestOverlayPermission()  // Android-only: оверлей-подсказка

// Списки (номера нормализуются к E.164; невалидные вернутся в skipped)
const result = await screening.setBlockedNumbers(['+77019999999', '8 701 123-45-67'])
if (result?.skipped.length) console.warn('Невалидные номера:', result.skipped)

await screening.setIdentifiedNumbers([{ number: '+77015550001', label: 'Банк QOR' }])

// Правила (Android; на iOS игнорируются с warning)
await screening.setRules({
  blockHiddenNumbers: true,
  blockNotInContacts: false,
  blockByPrefixes: ['+7495'],
  onlineLookup: { url: 'https://api.example.com/check', timeoutMs: 2500, failOpen: true },
})

// Загрузка списков в систему. На iOS это обязательный шаг
// (CXCallDirectoryManager.reloadExtension); на Android — дёшево.
const refresh = await screening.refresh()
if (refresh && !refresh.ok) console.warn('refresh:', refresh.error) // например extensionDisabled

// Реактивное состояние
screening.status        // ScreeningStatus | null
screening.capabilities  // CapabilityReport | null — контракт деградации
screening.log           // журнал; события callScreened добавляются live
screening.supported     // false на web / без плагина — ничего не падает

Важно про set* — это полная замена списка. Для инкрементальных правок — addBlockedNumbers / removeBlockedNumbers.

Обработка деградации

Не прячьте функциональность за if (granted) — показывайте состояние и способ восстановления. Всю карту рисует QorProtectionStatus, вручную это выглядит так:

const status = await screening.refreshStatus()
const label = status?.capabilities.callerLabel
// { state: 'degraded', reason: 'no_overlay_permission', remediation: 'requestOverlayPermission' }
if (label?.remediation) await screening.remediate(label.remediation)

Полная таблица причин/лестниц: packages/call-screening/docs/degradation-guide.md.

Чистый Capacitor (без Nuxt)

import { CallScreening } from '@qor/call-screening'

await CallScreening.requestScreeningRole()
await CallScreening.setBlockedNumbers({ numbers: ['+77019999999'] })
await CallScreening.refresh()
CallScreening.addListener('capabilityChanged', ({ capabilities }) => { /* re-render */ })

Рабочий пример — packages/call-screening/example (vite + vanilla TS, экран состояния защиты, списки, журнал).

Android: что нужно знать

  • Роль ROLE_CALL_SCREENING — единственный обязательный онбординг-шаг; без неё работают только checkNumber и списки «впрок» (режим postCallInfo).
  • Оверлей опционален: без него метка приходит heads-up-уведомлением (callerLabel: degraded).
  • Скрининг работает при убитом процессе приложения: систему сама поднимает сервис, решение пишется в журнал, события доставляются при следующем старте.
  • Событие callScreened приходит в JS только если процесс жив; источник истины — getScreeningLog().
  • На агрессивных прошивках (MIUI/EMUI/…) плагин выставит vendor_restriction — покажите кнопку openVendorSettings().
  • Онлайн-проверка: GET {url}?number=E164 → { verdict: 'spam'|'ok'|'unknown', label?, score? }; таймаут/ошибка → failOpen.

iOS: Call Directory Extension

Один раз на приложение добавляется extension-таргет:

  • Вариант A (вручную, 5–10 минут): packages/call-screening/docs/ios-extension-setup.md.
  • Вариант B (скрипт): pnpm exec capacitor-call-screening setup-ios --team TEAMID — правит pbxproj с полным откатом при ошибке; шаги подписи (App Groups) останутся ручными.

Затем в capacitor.config.ts:

plugins: {
  CallScreening: {
    iosAppGroup: 'group.com.acme.app',
    iosExtensionBundleId: 'com.acme.app.CallDirectory',
  },
},

Пользователь включает расширение в Настройки → Телефон → Блокировка и идентификация (requestScreeningRole() открывает этот экран). После любых изменений списков вызывайте refresh(). Расширение не работает в симуляторе — только реальное устройство.

iOS 18+: real-time идентификация через Live Caller ID Lookup — configureLiveLookup({ serviceURL, token }); серверная часть: packages/call-screening/docs/live-lookup-server.md.

Тестирование на эмуляторе Android

# собрать и поставить example
cd packages/call-screening/example
npm install && npm run build && npx cap sync android
cd android && JAVA_HOME=$(/usr/libexec/java_home -v 21 2>/dev/null || echo /opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home) ./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

# выдать роль/разрешения без UI (удобно для CI)
adb shell cmd role add-role-holder --user 0 android.app.role.CALL_SCREENING com.qor.callscreening.example
adb shell appops set com.qor.callscreening.example SYSTEM_ALERT_WINDOW allow
adb shell pm grant com.qor.callscreening.example android.permission.POST_NOTIFICATIONS

# имитация звонков (консоль эмулятора)
adb emu gsm call +77019999999    # входящий звонок
adb emu gsm cancel +77019999999  # сброс

Проверенный сценарий приёмки: номер из блок-листа не звонит и попадает в журнал как blocked/blocklist; номер из списка идентификации звонит с оверлеем-меткой (labeled/identified) — в том числе после am kill процесса приложения.

Нюансы:

  • Номер в gsm call передавайте в E.164 с + — консоль эмулятора отдаёт его сервису как есть, и без + номер не совпадёт со списками.
  • DNS-форвардинг эмулятора флапает: нативный HttpURLConnection может не резолвить хосты (Unable to resolve host), из-за чего onlineLookup молча уходит в failOpen. Запускайте эмулятор с -dns-server 8.8.8.8 — на реальных устройствах проблемы нет (домены публично резолвятся).