Встраивание чата поддержки в iOS-приложение
Для команд мобильных приложений. SDK iOS — Swift Package ShtabHelpdesk. Проверить встраивание до своей установки можно в песочнице мобильных SDK. Android — отдельное руководство, API тот же по смыслу.
Beta 1.0.0-beta.2. Изменения и известные ограничения версии приходят с комплектом поставки. До 1.0 API может меняться, после — только патчи 1.0.x.
1. Как устроено
Тонкий натив и весь чат в WKWebView. SDK показывает свой экран с WebView, который грузит UI чата с инсталляции — https://<публичный домен>/widget/mobile/. Функции чата приходят в приложение обновлением сервера, без нового SDK и ревью App Store.
Если UI инсталляции недоступен — нет сети при открытии чата, сервер старше и не знает /widget/mobile/, UI не поддерживает версию моста SDK (bridge.json), страница не загрузилась или не ответила за 20 с, — SDK показывает вшитую копию UI той же сборки. Она одна страница, и WebKit получает её как ответ адреса инсталляции (loadSimulatedRequest): сессия, очередь неотправленных сообщений и realtime у обеих копий общие.
Натив даёт UI через мост (WKScriptMessageHandlerWithReply): chat token приложения, push-токен устройства, открытие обращения из push, выбор файлов, переход по ссылкам в браузер, состояние приложения (в фоне realtime отключается).
Видео, видеокружки и стикеры UI играет в ленте своими элементами управления (allowsInlineMediaPlayback); без нажатия — только беззвучное, со звуком — по нажатию. На весь экран видео в SDK не разворачивается.
2. Требования
| iOS | 15.4 и новее, iPhone и iPad: WebKit до 15.4 не знает @layer, :has() и crypto.randomUUID, на которых стоят UI чата и UI kit. Deployment target пакета — 15.4: приложение с более низким target пакет не подключит — поднимите его до 15.4 (все устройства iOS 15 обновляются до 15.8) |
| Сборка приложения | Xcode 16+ (Swift 6 toolchain), Swift 5 или 6 в приложении |
| Зависимости SDK | только системные: UIKit, WebKit, PhotosUI, UniformTypeIdentifiers, UserNotifications, Network, CryptoKit, Security |
| Инсталляция | публичный домен с /widget/mobile/ и public API (сертификат публичного CA — ATS), размещение приложения (авторизованная зона, «Интеграции → Мобильные приложения»), издатель chat token (TrustedIssuer) |
3. Подключение
Офлайн-комплект релиза, mobile/ios/:
ShtabHelpdesk/— локальный Swift Package:Package.swiftиShtabHelpdesk.xcframework. Скопируйте папку в репозиторий приложения, в Xcode — «File → Add Package Dependencies… → Add Local…», или в своёмPackage.swift:.package(path: "third-party/ShtabHelpdesk").ShtabHelpdesk-<версия>.xcframework.zipи.sha256— для своего приватного SPM-репозитория:.binaryTarget(name: "ShtabHelpdesk", url: "https://…/ShtabHelpdesk-1.0.0-beta.2.xcframework.zip", checksum: "<sha256>").
Privacy manifest SDK (PrivacyInfo.xcprivacy) входит в framework: отслеживания нет, данные — «Customer Support» (связаны с пользователем, для функций приложения), UserDefaults — причина CA92.1. В «App Privacy» App Store Connect отметьте Customer Support.
4. Настройка и открытие чата
import ShtabHelpdesk
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
try? HelpdeskSDK.configure(
installationURL: URL(string: "https://support.example.com")!, // только origin, https
placementKey: "pk_…", // ключ приложения из консоли
options: HelpdeskOptions(appVersion: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String)
)
HelpdeskSDK.setChatTokenProvider {
// Бэкенд приложения подписывает chat token для вошедшего пользователя:
// iss, aud = ключ размещения, sub, exp ≤ 10 мин, jti. Нет пользователя — nil.
try? await backend.issueSupportToken()
}
return true
}
}
// UIKit: кнопка «Поддержка»
HelpdeskSDK.presentChat(from: viewController)
// SwiftUI
.sheet(isPresented: $support) { HelpdeskChatView() }
configureбросаетHelpdeskErrorпри неверном адресе, ключе, языке, версии или пине.http://принимается только в отладочной сборке приложения (симулятор, профиль разработки) — для стенда; приложению тогда нужно исключение ATS (NSAllowsLocalNetworkingдляlocalhost).- Провайдер вызывается при каждом открытии чата и когда сессия чата кончилась; вариант с completion —
setChatTokenProvider { completion in … completion(token) }(один раз, с любого потока). Токен одноразовый, SDK его не хранит. Нет ответа за 15 с — чат открывается без входа и показывает «Войдите в приложении снова». - В приложении сессия чата начинается только с chat token приложения: cookie, оставшаяся от прежнего пользователя, сессию не возвращает.
- События:
HelpdeskSDK.delegate(helpdeskUnreadChanged(_:),helpdeskSessionExpired()) илиHelpdeskSDK.onEvent(.unreadChanged(n),.sessionExpired). - Весь API — главный поток (
@MainActor). Экран можно встроить и в свою навигацию:HelpdeskSDK.makeChatViewController().
5. Выход и смена пользователя
При выходе пользователя из приложения вызовите HelpdeskSDK.logout(): экран чата закрывается (модальный или на вершине стека навигации; экран, встроенный во вкладку, корень стека или destination SwiftUI, остаётся на месте, но страница прежнего пользователя сразу выгружается, а после очистки чат открывается заново для следующего), сессия завершается на сервере (сервер отзывает push этого устройства), cookie сессии и очередь неотправленных сообщений WebView удаляются, обращение из push забывается. Нет сети — сервер узнает при следующем configure. Следующий пользователь входит своим chat token и не видит данных прежнего.
На iOS 17+ у чата своё хранилище WebView; на iOS 15–16 — хранилище приложения по умолчанию, и logout удаляет данные WebView для домена инсталляции во всём приложении (если приложение само открывает страницы этого домена в своих WebView, их cookie тоже уйдут).
6. Push (APNs)
Сервер шлёт push без текста: alert «Новый ответ — Откройте чат поддержки, чтобы прочитать» на языке размещения, рядом с aps — плоские ключи conversation_id и kind (new_reply; тестовый из «Проверка приёма» — test без обращения). Окружение APNs, Team ID, Key ID и ключ .p8, Bundle ID задаются в консоли («Интеграции → Мобильные приложения → iOS»). Приложению нужна capability Push Notifications.
// AppDelegate
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
HelpdeskSDK.registerPushToken(deviceToken)
}
// UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification,
withCompletionHandler completion: @escaping (UNNotificationPresentationOptions) -> Void) {
// Чат на экране — ответ уже виден, баннер не нужен; не наш push — решает приложение.
completion(HelpdeskNotifications.presentationOptions(for: notification) ?? [.banner, .sound])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse,
withCompletionHandler completion: @escaping () -> Void) {
HelpdeskSDK.handleNotification(response, presentingFrom: rootViewController) // true — push инсталляции
completion()
}
- Разрешение на уведомления приложение спрашивает само, с объяснением и кнопкой «Отложить» (как в sample), затем
registerForRemoteNotifications(). - Обращение из push открывается после входа пользователя чата; чужое обращение показывается как «Нет доступа к обращению». С
presentingFromчат показывается сразу, без него — при следующемpresentChat. - Push-токен хранится в SDK (Keychain) и регистрируется для клиента при каждом входе в чат: новый токен, полученный при закрытом чате, уходит на сервер при следующем открытии.
- Проверка без сервера: в отладочной сборке
HelpdeskDebug.testPushUserInfo(conversationID:)даёт тот жеuserInfo, что шлёт сервер, аHelpdeskDebug.scheduleTestNotification(conversationID:after:)ставит такое локальное уведомление — его тап приходит в делегат как настоящий push. В сборках TestFlight и App Store оба бросаютHelpdeskError.debugOnly.
7. Файлы
Скрепка в чате открывает выбор: «Фото» (PHPickerViewController, доступа к медиатеке не нужно; HEIC приходит как JPEG), «Камера» — только если в Info.plist приложения есть NSCameraUsageDescription, «Файлы» (UIDocumentPickerViewController). Только изображения и PDF, до 10 за раз. Размер сверх лимита UI отклоняет до загрузки; лимит и типы организации проверяет сервер и объясняет отказ в чате. Файл передаётся странице по одноразовому handle кусками по мосту, путь на устройстве UI не видит; копии SDK удаляются с экраном.
«Скачать» у файла в чате: UI отдаёт байты файла SDK по мосту, SDK показывает системное «Поделиться» с файлом — «Сохранить в Файлы», AirDrop, почта. Доступа к медиатеке и ключей Info.plist для этого не нужно. На iPad окно открывается поповером. Копия SDK лежит во временной папке приложения и удаляется с экраном чата.
8. Безопасность
| Что | Где |
|---|---|
| chat token приложения | не хранится: используется один раз |
| сессия чата | короткий bearer — в памяти страницы; cookie сессии (HttpOnly) — в хранилище WebView, удаляется при logout |
| push-токен, обращение из push, отложенный logout | Keychain, сервис ru.shtab.helpdesk, AfterFirstUnlockThisDeviceOnly — не в резервной копии, не на новом устройстве; стирается при первом запуске после переустановки |
| неотправленные сообщения | IndexedDB WebView (origin инсталляции), удаляется при logout |
- Ошибка TLS — отказ: SDK не принимает недоверенные сертификаты.
- Мост принимает сообщения только из главного фрейма origin инсталляции и только от страницы UI; ссылки и любые переходы с неё открываются в браузере; запросы страницы к камере и микрофону отклоняются.
- App-Bound Domains. Если в Info.plist приложения есть
WKAppBoundDomains, добавьте туда домен инсталляции: иначе WebKit не даст открыть чат и отключит мост (вшитая страница грузится как HTML-навигация приложения). Без этого ключа ограничений нет.
Certificate pinning (по умолчанию выключено)
HelpdeskOptions(certificatePins: ["sha256/<основной ключ>", "sha256/<резервный ключ>"])
Пины — SHA-256 SubjectPublicKeyInfo в base64 (ключ сервера или CA заказчика, всегда с резервным; ключи RSA и EC P-256/384/521). С пинами SDK проверяет сертификат своих запросов (bridge.json, logout) и TLS-соединений WebView к инсталляции — страницы, config.js, API (WKNavigationDelegate, после обычной проверки цепочки). Не совпали — чат не открывается («Не удалось проверить сервер поддержки»).
Ограничение. Передаёт ли WebKit в этот делегат проверку соединения WebSocket realtime — подтверждается на устройстве . Если нет, WebSocket защищён только обычной проверкой цепочки: это остаточный риск.
9. Диагностика
HelpdeskOptions(logger: { level, code in … }) — только коды, без текста, токенов и персональных данных; по умолчанию warning/error — в системный журнал (subsystem ru.shtab.helpdesk). Коды те же, что у Android (§9), кроме проверки версии WebView (webview.version_*, webview.outdated: на iOS WebKit — часть системы, минимум задаёт deployment target 15.4), плюс:
| Код | Что значит |
|---|---|
ui.bundled.missing | в сборке SDK нет вшитой страницы (собран без bundle-widget-ui.sh) |
ui.page_http_<код> | страница UI инсталляции ответила ошибкой — открыт вшитый UI |
webview.process_gone | iOS завершил процесс страницы (память, фон) — чат загружен заново |
file.unknown, file.too_large, file.unreadable | страница просила файл, которого SDK не даст |
bridge.refused, bridge.post_failed | сообщение не из страницы UI / ответ странице не доставлен |
store.keychain_<статус> | Keychain не сохранил значение |