- Print
Playing a webview campaign list with the native player
- Print
When integrating a hybrid app, you do not need to rebuild the campaign list natively. Keep the plugin list rendered inside the webview, and hand off to native playback only when a campaign is tapped.

Comparing the two approaches
Build the list natively as well You implement the list UI natively and open a preview screen per item. Composing the list, paging, and refreshing state are all yours to manage. | Reuse the webview list (this guide) The plugin's |
The webview and native are separate layers. Native already holds the accessKey, so the only value passed from the webview to native is
campaignKey.
Flow
- Webview — renders the list and passes
campaignKeyto native on tap - Native — receives the value
- Native — plays it with the SDK
Step 1. Render the list and pass the key to native on tap
setOverall renders the list. When no native interface exists, the click callback falls back to the default web modal player, so the same page is safe to ship both on the web and inside a hybrid app.
On iOS, inline playback requires
allowsInlineMediaPlayback = trueinWKWebViewConfiguration. (This is a webview setting, independent of the code below.)
JavaScript
<script src="https://static.shoplive.cloud/shoplive.js"></script>
<script>
var accessKey = 'YOUR_ACCESS_KEY';
var messageCallback = {
ON_CLICK_CAMPAIGN_LIST_ITEM: function (payload) {
var APP_INTERFACE = "ShopLiveAppInterface";
var campaignKey = payload.campaignKey;
if (window[APP_INTERFACE]) {
// Android, Flutter - both support postMessage(String) only
window[APP_INTERFACE].postMessage(campaignKey);
} else if (window.webkit?.messageHandlers?.[APP_INTERFACE]) {
// iOS
window.webkit.messageHandlers[APP_INTERFACE].postMessage(campaignKey);
} else {
// No native interface (plain web) - fall back to the web modal
cloud.shoplive.showFeaturedPlayerModal({ campaignKey: campaignKey });
}
}
};
cloud.shoplive.init({ accessKey: accessKey, messageCallback: messageCallback });
</script>
<div id="shoplive-overall"></div>
<script defer>
cloud.shoplive.setOverall('shoplive-overall');
</script>Step 2. Receive the value on native
Register the interface name used above (ShopLiveAppInterface) on the native side. This registration is not a Shoplive SDK feature — it is the standard JS-to-native bridge each webview provides. Only the campaignKey string is exchanged, so all three platforms receive it as-is with no parsing.
On iOS,
userContentController.add()must be called before the WKWebView is created, that is, before building the webview with this configuration.
Kotlin · Android
webView.addJavascriptInterface(ShopLiveAppInterface(activity), "ShopLiveAppInterface")
class ShopLiveAppInterface(val activity: Activity) {
@JavascriptInterface
fun postMessage(campaignKey: String) {
activity.runOnUiThread {
/* Step 3: call ShopLive.play() */
}
}
}Swift · iOS
configuration.userContentController.add(self, name: "ShopLiveAppInterface")
func userContentController(_ controller: WKUserContentController,
didReceive message: WKScriptMessage) {
guard message.name == "ShopLiveAppInterface",
let campaignKey = message.body as? String else { return }
// Step 3: call ShopLive.play()
}Dart · Flutter
controller.addJavaScriptChannel(
'ShopLiveAppInterface',
onMessageReceived: (JavaScriptMessage message) {
final campaignKey = message.message;
// Step 3: call ShopLive.play()
},
);Any JavaScript running inside the webview can call this bridge. If the webview can navigate to URLs other than your own page (product links, for example), those pages can call
ShopLiveAppInterfacetoo. We recommend opening external destinations in a new window or the external browser so that only your page stays inside the webview.
Step 3. Play on native
The single campaignKey received through the bridge is enough for all three platforms to open the full player in the same shape. (Guest playback, assuming the native SDK is already initialized with the accessKey — Android ShopLive.setAccessKey, iOS ShopLive.configure.)
Kotlin · Android
ShopLive.play(activity, ShopLivePlayerData(campaignKey))Swift · iOS
ShopLive.play(data: .init(campaignKey: campaignKey))Dart · Flutter
shopLivePlayer.play(data: ShopLivePlayerData(campaignKey: campaignKey));Playing as a logged-in user (optional)
Skip this step for guest playback. Two things need to be decided — which authentication method to use, and where the value comes from.
Authentication method
A · Advanced (JWT) authentication Verified with a JWT signed by your server. | B · Simple authentication Passes only |
A · Advanced authentication (JWT)
Kotlin · Android
ShopLive.setAuthToken(jwt) // before play()Swift · iOS
ShopLive.authToken = jwtDart · Flutter
shopLiveCommon.setAuthToken(userJWT: jwt);The JWT is signed by your own server using the secret key issued by Shoplive. Issue it for the same user as the JWT used by the webview plugin.
B · Simple authentication (userId)
Kotlin · Android
ShopLive.setUser(ShopLiveCommonUser(userId))Swift · iOS
ShopLiveCommon.setUser(user: ShopLiveCommonUser(userId: userId))Dart · Flutter
shopLiveCommon.setUser(user: ShopLiveCommonUser(userId: userId));Where the value comes from
Where you obtain the authentication value before calling play() depends on how login is structured in your hybrid app.
A · Native manages login The native app already holds the value. Leave the bridge payload as is and simply call the API above with that value right before | B · Login lives only in the webview session Native does not know the user. Send the value together in the Step 1 bridge payload, and have native call the API above with it. |
For case B a single string is not enough: change the Step 1 code to send an object carrying the value, and change Step 2 to parse JSON/an object instead of a string. (The example below uses JWT.)
JavaScript · extending the Step 1 code
var bridgePayload = { campaignKey: campaignKey, authToken: currentUserJWT };
if (window[APP_INTERFACE]) {
window[APP_INTERFACE].postMessage(JSON.stringify(bridgePayload));
} else if (window.webkit?.messageHandlers?.[APP_INTERFACE]) {
window.webkit.messageHandlers[APP_INTERFACE].postMessage(bridgePayload);
}