Deep links и mobile bridge
Этот раздел фиксирует рабочий контракт для открытия стрима в WebView и передачи действий пользователя в нативный слой.
URL открытия стрима в WebView
Базовый формат:
https://<client-domain>/live/?wv=1#/translation/<translationId>
Для устройств с вырезом экрана (чёлка / Dynamic Island) добавляйте safe_area=1:
https://<client-domain>/live/?wv=1&safe_area=1#/translation/<translationId>
Fallback, если у клиента нет собственной страницы стримов:
https://app.shopstory.live/<client-name>/?wv=1#/translation/<translationId>
Кнопка закрытия WebView должна быть реализована в нативном приложении, не внутри страницы стрима.
Контракт событий из WebView
Страница стрима отправляет в нативный слой два события:
add-product— пользователь нажал «Купить»open-product— пользователь открыл карточку товара
Payload события передаётся как JSON-строка (результат JSON.stringify). Нативный слой должен распарсить строку для доступа к полям:
namefeedProductIdfeedProductGroupIdurlvendorCode
Подключение обработчиков в нативном приложении
Самая частая причина того, что события не приходят — обработчики инициализируются после загрузки страницы или с неправильным именем. Регистрируйте их до вызова loadRequest.
- iOS (Swift)
- Android (Kotlin)
import WebKit
class ShopstoryWebViewController: UIViewController, WKScriptMessageHandler {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
// Обязательно для inline-воспроизведения видео
// (без этого iOS открывает видео в нативном полноэкранном плеере)
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)
view.addSubview(webView)
let url = URL(string: "https://example.ru/live/?wv=1&safe_area=1#/translation/123")!
webView.load(URLRequest(url: url))
}
// Обработка событий от ShopStory
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard let jsonString = message.body as? String,
let data = jsonString.data(using: .utf8),
let product = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return }
let feedProductId = product["feedProductId"] as? String ?? ""
let name = product["name"] as? String ?? ""
let url = product["url"] as? String ?? ""
switch message.name {
case "add-product":
// Добавить товар в корзину по feedProductId
CartManager.shared.addProduct(id: feedProductId)
case "open-product":
// Открыть карточку товара в нативном UI
navigator.openProduct(url: url)
default:
break
}
}
}
import android.webkit.JavascriptInterface
import android.webkit.WebView
import org.json.JSONObject
class ShopstoryWebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val webView = WebView(this)
webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false // Разрешить автовоспроизведение
// Регистрация bridge — ДО загрузки страницы
webView.addJavascriptInterface(
ShopstoryBridge(),
"shopstory" // имя должно быть именно "shopstory"
)
webView.loadUrl("https://example.ru/live/?wv=1&safe_area=1#/translation/123")
setContentView(webView)
}
inner class ShopstoryBridge {
@JavascriptInterface
fun addProduct(jsonString: String) {
// Вызывается при нажатии «Купить»
val product = JSONObject(jsonString)
val feedProductId = product.optString("feedProductId")
// Добавить товар в корзину по feedProductId
CartManager.addProduct(feedProductId)
}
@JavascriptInterface
fun openProduct(jsonString: String) {
// Вызывается при клике на карточку товара
val product = JSONObject(jsonString)
val url = product.optString("url")
// Открыть карточку товара в нативном UI
navigator.openProduct(url)
}
}
}
Что отправляет ShopStory (JS-сторона)
Для справки — так выглядит отправка событий со стороны SDK. Этот код уже встроен в ShopStory, вам его реализовывать не нужно:
- iOS
- Android
const PRODUCT = JSON.stringify({
name,
feedProductId,
feedProductGroupId,
url,
vendorCode,
});
// PRODUCT — строка (JSON.stringify), не объект.
// iOS: let data = try JSONSerialization.jsonObject(with: product.data(using: .utf8)!)
window.webkit.messageHandlers['add-product'].postMessage(PRODUCT);
window.webkit.messageHandlers['open-product'].postMessage(PRODUCT);
const PRODUCT = JSON.stringify({
name,
feedProductId,
feedProductGroupId,
url,
vendorCode,
});
// PRODUCT — строка (JSON.stringify), не объект.
// Android: val json = JSONObject(product)
window.shopstory.addProduct(PRODUCT);
window.shopstory.openProduct(PRODUCT);
Отладка
Если события не приходят в нативный слой:
- Убедитесь, что URL содержит
?wv=1. Без этого параметра bridge не активируется. - Проверьте, что обработчики зарегистрированы до вызова
loadRequest/loadUrl. - Откройте страницу с параметром
__DEV__==__DEV__— SDK покажет в консоли предупреждения видаglobal.shopstory.addProduct is not a function, которые укажут на незарегистрированные обработчики. - Для быстрой проверки подставьте mock-обработчики через консоль WebView:
// iOS mock
window.webkit = { messageHandlers: {
'add-product': { postMessage: function(p) { console.log('add-product', p); } },
'open-product': { postMessage: function(p) { console.log('open-product', p); } },
}};
// Android mock
window.shopstory = {
addProduct: function(p) { console.log('addProduct', p); },
openProduct: function(p) { console.log('openProduct', p); },
};
После тапа на товар или кнопку «Купить» в консоли должен появиться JSON с payload.
Чек-лист мобильной интеграции
- WebView открывает URL с
wv=1(иsafe_area=1для устройств с вырезом). - (iOS)
WKWebViewConfigurationвключаетallowsInlineMediaPlayback = trueиmediaTypesRequiringUserActionForPlayback = []. - (Android)
WebView.settings.mediaPlaybackRequiresUserGesture = false. - Обработчики
add-productиopen-productзарегистрированы до загрузки страницы. - При получении
add-productприложение добавляет товар в корзину поfeedProductId. - При получении
open-productприложение открывает карточку товара поurl. - Кнопка закрытия WebView реализована нативно.
- Для fallback-сценария согласован URL
app.shopstory.live/<client-name>.
Частые проблемы
| Симптом | Причина | Решение |
|---|---|---|
| События не приходят | Обработчики зарегистрированы после загрузки страницы | Регистрировать до loadRequest / loadUrl |
| События не приходят | wv=1 отсутствует в URL | Добавить ?wv=1 в query-параметры |
| Клик «Купить» ничего не делает | Имя interface не совпадает | Android: именно "shopstory", iOS: именно "add-product" |
| Payload приходит как строка, а не объект | SDK отправляет JSON.stringify | Распарсить через JSONSerialization (iOS) или JSONObject (Android) |
| Контент перекрывается вырезом экрана | Не передан safe_area=1 | Добавить &safe_area=1 в URL |
| Добавляется не тот товар | Используется vendorCode вместо feedProductId | Для корзины использовать feedProductId (= <offer id> из фида) |
| Видео открывается в нативном полноэкранном плеере iOS | allowsInlineMediaPlayback не включён в WKWebViewConfiguration | Добавьте config.allowsInlineMediaPlayback = true и config.mediaTypesRequiringUserActionForPlayback = [] |
| Видео не воспроизводится автоматически на Android | mediaPlaybackRequiresUserGesture не отключён | Добавьте webView.settings.mediaPlaybackRequiresUserGesture = false |
Связанные разделы: