Перейти к основному содержимому
Версия: next

Встраивание чата поддержки в 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. Требования​

iOS15.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, отложенный logoutKeychain, сервис 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_goneiOS завершил процесс страницы (память, фон) — чат загружен заново
file.unknown, file.too_large, file.unreadableстраница просила файл, которого SDK не даст
bridge.refused, bridge.post_failedсообщение не из страницы UI / ответ странице не доставлен
store.keychain_<статус>Keychain не сохранил значение