Интеграция
Установка
В приложение (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— на реальных устройствах проблемы нет (домены публично резолвятся).