Перейти к основному содержимому

Интеграция в мобильное приложение

Плеер ShopStory воспроизводит стрим, показывает товары и чат. Когда пользователь нажимает «Купить» или открывает товар — плеер отправляет событие в ваше приложение через bridge.

Плеер всегда работает через WebView (WKWebView на iOS, WebView на Android) — нативного SDK для плеера нет. Но для списка стримов есть два варианта.

Два варианта интеграции

Вариант A: Всё через WebView (быстрый)

И список стримов, и плеер открываются в WebView. Минимум нативного кода.

Нативное приложение → WebView со списком стримов →
→ тап на стрим → плеер открывается в том же WebView

Как реализовать: откройте в WebView страницу https://<ваш-домен>/live/?wv=1. ShopStory отрисует список стримов и плеер.

Плюсы: минимум работы, обновления UI приходят автоматически. Минусы: нет контроля над дизайном списка стримов, не нативный UX.

Вариант B: Нативный список + WebView плеер (рекомендуемый)

Список стримов отрисовывает ваше приложение через API /v3/streams. При тапе на стрим — открывается WebView с плеером.

Нативное приложение → API /v3/streams → нативный список стримов →
→ тап на стрим → WebView с плеером

Как реализовать: запросите стримы через API, отрисуйте нативные карточки, по тапу откройте WebView с URL плеера.

Плюсы: полный контроль над дизайном списка, нативный UX, можно фильтровать стримы. Минусы: больше работы на стороне приложения.

Плеер — только WebView

Независимо от выбранного варианта, сам плеер стрима всегда открывается в WebView. Остальная часть этой страницы описывает настройку WebView для плеера.

Перед началом

Для интеграции вам понадобится:

  1. applicationId вашего проекта в ShopStory (выдаётся при подключении).
  2. URL страницы стримовhttps://<ваш-домен>/live/ или fallback https://app.shopstory.live/sdk/.
  3. ID стрима (translationId) — получите через GET /v3/streams или из личного кабинета.

Как получить ID стрима (вариант B)

Ваше приложение запрашивает список стримов через API и показывает их в нативном UI. При тапе на стрим — открывает WebView с плеером.

GET https://app.shopstory.live/v3/streams?applicationId=<ваш-id>

Ответ содержит массив стримов с полем id — это и есть translationId для URL плеера. Подробнее: Каталог стримов.

Быстрый старт

1. Откройте URL стрима в WebView

https://<ваш-домен>/live/?wv=1#/translation/<translationId>

Параметр wv=1 обязателен — он активирует мобильный режим плеера.

2. Настройте WebView

let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true // видео внутри страницы, не fullscreen
config.mediaTypesRequiringUserActionForPlayback = [] // автовоспроизведение без тапа

config.userContentController.add(self, name: "add-product")
config.userContentController.add(self, name: "open-product")

let webView = WKWebView(frame: view.bounds, configuration: config)

3. Обработайте события от плеера

func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard let json = message.body as? String,
let data = json.data(using: .utf8),
let product = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return }

let feedProductId = product["feedProductId"] as? String ?? ""
let url = product["url"] as? String ?? ""

switch message.name {
case "add-product":
CartManager.shared.addProduct(id: feedProductId)
case "open-product":
navigator.openProduct(url: url)
default: break
}
}

4. Добавьте кнопку «Закрыть»

В мобильном режиме плеер скрывает свою встроенную кнопку навигации — пользователь не сможет выйти из просмотра без вашей помощи. Варианты:

  • Нативная кнопка поверх WebView — крестик или стрелку в safe area (пример в полном коде ниже). Плеер резервирует 50px слева в шапке, чтобы ваша кнопка не перекрывала заголовок.
  • Системная навигация — разместите WebView внутри UINavigationController (iOS) или Activity с ActionBar (Android).

Типичный флоу пользователя

Нативный список стримов → тап на стрим → WebView с плеером →
→ пользователь смотрит, тапает «Купить» →
→ bridge-событие add-product → нативное приложение добавляет в корзину →
→ пользователь закрывает WebView (нативная кнопка)

Когда стрим заканчивается: страница перезагружается и плеер переключается в режим записи (VOD). Пользователь может продолжить просмотр или закрыть WebView. Закрывать WebView программно не нужно.


Параметры URL

ПараметрОбязательныйОписание
wv=1ДаВключает мобильный режим. Без этого параметра: события add-product / open-product не будут отправляться, кнопка «Купить» будет открывать URL в новой вкладке вместо отправки события в приложение.
safe_area=1По ситуацииДобавляет отступы в плеере под вырез экрана (чёлка, Dynamic Island). См. подробности ниже.
startTime=<сек>НетНачать воспроизведение записи с указанной секунды. Предназначен для VOD — на live-стримах seek может не сработать.
hide_chat=1НетСкрыть чат эфира. Полезно если хотите максимально чистый вид плеера.

Когда использовать safe_area=1

  • Используйте, если ваш WebView занимает весь экран (edge-to-edge) и вы не добавляете свои отступы под вырез. Плеер сам сдвинет заголовок и элементы управления вниз.
  • Не используйте, если ваш WebView уже размещён ниже safe area (например, внутри UINavigationController или ниже собственного toolbar) — иначе отступ удвоится и сверху будет лишнее пустое пространство.
Замените домен

В примерах используется <ваш-домен>. Подставьте домен вашего интернет-магазина, где установлен ShopStory (например, www.example.ru). Если у вас нет собственной страницы стримов — используйте app.shopstory.live/sdk/ с параметром x-force-appid=<ваш-applicationId> (этот параметр заменяет привязку к домену).

События bridge

Плеер отправляет два события:

СобытиеКогдаiOSAndroid
add-productПользователь нажал «Купить»messageHandlers['add-product']shopstory.addProduct()
open-productПользователь открыл товарmessageHandlers['open-product']shopstory.openProduct()

После нажатия «Купить» кнопка в плеере автоматически меняется на «В корзине».

Payload

Payload передаётся как JSON-строка (не объект). Распарсите её для доступа к полям.

{
"name": "Название товара",
"feedProductId": "12345",
"feedProductGroupId": "",
"url": "https://example.ru/product/12345/",
"vendorCode": "ART-001"
}
ПолеВсегда присутствуетОписание
nameДаНазвание товара
feedProductIdДаID товара из фида. Используйте это поле для добавления в корзину.
feedProductGroupIdНетГруппа товара (для вариаций). Может быть пустой строкой.
urlДаURL карточки товара на сайте. Используйте для перехода к товару.
vendorCodeНетАртикул товара. Может отсутствовать.

Полный пример

ShopstoryWebViewController.swift
import WebKit

class ShopstoryWebViewController: UIViewController, WKScriptMessageHandler {

private var webView: WKWebView!
private let streamUrl: String // URL вида "https://example.ru/live/?wv=1#/translation/123"

init(streamUrl: String) {
self.streamUrl = streamUrl
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) { fatalError() }

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black

// 1. Настройка WebView
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
config.userContentController.add(self, name: "add-product")
config.userContentController.add(self, name: "open-product")

webView = WKWebView(frame: view.bounds, configuration: config)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)

// 2. Кнопка «Закрыть»
let closeButton = UIButton(type: .system)
closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
closeButton.tintColor = .white
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.addTarget(self, action: #selector(close), for: .touchUpInside)
view.addSubview(closeButton)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
closeButton.widthAnchor.constraint(equalToConstant: 32),
closeButton.heightAnchor.constraint(equalToConstant: 32),
])

// 3. Загрузка стрима
if let url = URL(string: streamUrl) {
webView.load(URLRequest(url: url))
}
}

@objc private func close() {
dismiss(animated: true)
}

// 4. Обработка событий
func userContentController(
_ uc: WKUserContentController, didReceive message: WKScriptMessage
) {
guard let json = message.body as? String,
let data = json.data(using: .utf8),
let product = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return }

let feedProductId = product["feedProductId"] as? String ?? ""
let url = product["url"] as? String ?? ""

switch message.name {
case "add-product":
CartManager.shared.addProduct(id: feedProductId)
case "open-product":
navigator.openProduct(url: url)
default: break
}
}
}

Как открыть из списка стримов:

let url = "https://example.ru/live/?wv=1#/translation/\(stream.id)"
let vc = ShopstoryWebViewController(streamUrl: url)
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)

Чек-лист

  • URL содержит ?wv=1
  • (iOS) allowsInlineMediaPlayback = true и mediaTypesRequiringUserActionForPlayback = []
  • (Android) mediaPlaybackRequiresUserGesture = false
  • Обработчики зарегистрированы до загрузки URL
  • Реализована нативная кнопка «Закрыть» / «Назад»
  • add-product → добавление в корзину по feedProductId
  • open-product → переход в карточку товара по url

Частые проблемы

СимптомРешение
События не приходятПроверьте ?wv=1 в URL и регистрацию обработчиков до loadRequest
Видео открывается в fullscreen-плеере (iOS)Добавьте allowsInlineMediaPlayback = true
Видео не воспроизводится автоматически (Android)Добавьте mediaPlaybackRequiresUserGesture = false
Контент перекрывается вырезом экранаДобавьте &safe_area=1 в URL
Двойной отступ под вырезУберите safe_area=1 — ваше приложение уже обрабатывает safe area
Payload приходит как строкаРаспарсите через JSONSerialization (iOS) или JSONObject (Android)
Android: события не приходятИмя interface должно быть "shopstory" (iOS: "add-product" и "open-product")

Отладка

  1. (iOS) Включите Safari Web Inspector для отладки WebView:

    #if DEBUG
    if #available(iOS 16.4, *) { webView.isInspectable = true }
    #endif

    Затем: Safari → Develop → Simulator (или устройство) → выберите страницу.

  2. При нажатии «Купить» без зарегистрированных обработчиков в консоли появится global.shopstory.addProduct is not a function — это значит bridge не настроен.

  3. Убедитесь что feedProductId в payload совпадает с <offer id> из вашего товарного фида.


Следующие шаги: