Shoplive Shortform SDK Integration Guide
This guide walks you through adding commerce shorts to your app, organized by
the case you want to build (a mission). Full-screen playback, embedded collections, floating previews,
product and share handling — pick only the parts you need.
Switch the code samples with the iOS / Android tabs at the top of each block.
Overview & Requirements
The Shoplive Shortform SDK brings commerce-oriented vertical shorts to your app. The SDK owns the whole shorts experience — playback, preloading, infinite scroll, product overlays, likes, comments and bookmarks. Your app only decides which shorts to show (the query) and where a tap should take the user (routing).
Module Structure
Mission map — what do you want to build?
| What you want | Mission |
|---|---|
| A button opens shorts full screen, swipeable up and down | Mission 1 · Open full screen |
| A collection of shorts inside your home or campaign screen | Mission 2 · Embed a collection |
| A small floating window with related shorts on a product page | Mission 3 · Floating preview |
| Signed-in users can like, comment and bookmark | Mission 4 · Connect user identity |
| A product tap opens your own detail page; sharing uses your own link | Mission 5 · Events, products, sharing |
| Play exactly the list and order your server decides (personalized feed) | Mission 6 · Play your own list |
| Hide the like or share button, or change what a card shows | Mission 7 · Customize the UI |
Delivery Forms (Platform Mapping)
| Form | iOS | Android |
|---|---|---|
| Full-screen playback | ShopLiveShortform.play(requestData:) | ShopLiveShortform.play(context, collectionData) |
| Collection (card / vertical / horizontal) | CardTypeViewBuilder · ListViewBuilder → getView() | ShopLiveShortformCardTypeView and friends, in XML |
| Full feed inside a screen | ShopLiveShortsCollectionView (UIView) | ShopLiveShortformDetailTypeView (XML view) |
| Floating preview | ShopLiveShortform.showPreview(requestData:) | ShopLiveShortform.showPreview(activity, data) |
| Receiving events | ShopLiveShortformReceiveHandlerDelegate | ShopLiveShortformHandler |
build(...).getView()); on Android you place a view in XML, configure it, then call submit().
The query options (tags, brands, SKUs, group) and the event model are the same on both sides,
so what you learn on one platform carries over to the other.Requirements
| Item | iOS | Android |
|---|---|---|
| Minimum version | iOS 11.0+ | minSdk 23+ with shoplive-media3 (the default engine) · 19+ with shoplive-exoplayer |
| Language / toolchain | Swift 5 | Kotlin · JDK 17. compileSdk 36 |
| SDK version | 1.8.13 (ShopliveShortformSDK) | 1.8.14 (shoplive-short-form) |
| Required companion modules | ShopliveSDKCommon (common-ios) | shoplive-common · shoplive-network · a player engine |
Installation
iOS
// ① Swift Package Manager
// Xcode ▸ File ▸ Add Package Dependencies…
dependencies: [
.package(url: "https://github.com/shoplive/shortform-ios", .upToNextMajor(from: "1.8.13")),
.package(url: "https://github.com/shoplive/common-ios", .upToNextMajor(from: "1.8.13")),
]
// ② CocoaPods — Podfile
// pod 'ShopliveShortformSDK', '1.8.13'
// pod 'ShopliveSDKCommon', '1.8.13'
// ③ Import
import ShopliveSDKCommon // access key, authentication, common models
import ShopLiveShortformSDK // shorts playback, lists, preview
// settings.gradle / build.gradle — mavenCentral() required
dependencies {
implementation("cloud.shoplive:shoplive-short-form:1.8.14") // shorts
implementation("cloud.shoplive:shoplive-common:1.8.14") // shared (access key, auth)
implementation("cloud.shoplive:shoplive-network:1.8.14") // networking
// Playback engine — declare <b>only one</b> of these (engine versions are numbered separately from the SDK)
implementation("cloud.shoplive:shoplive-media3:1.10.1.3") // media3 (default) — minSdk 23
// implementation("cloud.shoplive:shoplive-exoplayer:2.19.1.20") // ExoPlayer (legacy) — minSdk 19
}
android {
defaultConfig { multiDexEnabled = true }
}
shoplive-short-form references the shared, networking and player-engine modules as compileOnly.
That means if you don't declare them in your app module, the classes are missing at runtime.
Use shoplive-media3 by default, or shoplive-exoplayer if your app still uses legacy ExoPlayer.Where each class comes from
| Class | Module (iOS) | Package (Android) |
|---|---|---|
ShopLiveCommon · ShopLiveCommonError | ShopliveSDKCommon | cloud.shoplive.sdk.common |
ShopLiveShortform, collections, preview | ShopLiveShortformSDK | cloud.shoplive.sdk.shorts |
| Product and shorts response models | ShopliveSDKCommon (ProductData etc.) | cloud.shoplive.sdk.network.response |
Quick Start
Once installation is done, the code below plays your first short.
accessKey is issued by Shoplive. If you don't have one, ask your Shoplive contact
or look it up in Shoplive Admin. Shorts need no campaign key — you choose what plays with a
query (tags, brands, SKUs, group ID).Full-screen playback — 2 lines
// ① Once at app startup (AppDelegate)
ShopLiveCommon.setAccessKey(accessKey: "YOUR_ACCESS_KEY")
// ② Play — with no query, you get every short
ShopLiveShortform.play(requestData: ShopLiveShortformCollectionData(delegate: nil))
// ① Once at app startup (Application.onCreate)
ShopLiveCommon.setAccessKey("YOUR_ACCESS_KEY")
// ② Play — with no query, you get every short
ShopLiveShortform.play(context)
Those two lines load the shorts, play them in a vertical full-screen feed, and bring along the product cards, likes, comments and share overlays. For details: 1 connect user identity · 2 Mission 1.
setAccessKey must run before playback. If it hasn't, you get
9000 NOT_INITIALIZED_ACCESS_KEY (error handling).
Do it in AppDelegate / Application.onCreate.Playing with a query
let data = ShopLiveShortformCollectionData(
tags: ["spring", "outerwear"],
tagSearchOperator: .AND, // match every tag (.OR = match any)
brands: ["shoplive"],
shuffle: true,
skus: ["SKU-1001"],
shortsCollectionId: "COLLECTION_ID",
isMuted: false,
delegate: self // receive events — Mission 5
)
ShopLiveShortform.play(requestData: data)
val data = ShopLiveShortformCollectionData().apply {
tags = listOf("spring", "outerwear")
tagSearchOperator = ShopLiveShortformTagSearchOperator.AND
brands = listOf("shoplive")
shuffle = true
skus = listOf("SKU-1001")
shortsCollectionId = "COLLECTION_ID"
isMuted = false
handler = myHandler // receive events — Mission 5
}
ShopLiveShortform.play(context, data)
Next stepScreen Terminology Guide · Start with Mission 1 →
Screen Terminology Guide
This section names every element on screen. The missions, options and events below use the same names. Sign-in required marks elements that only work once the user is identified (Mission 4).
Detail screen (full-screen playback)
A vertical full-screen feed you swipe up and down. Missions 1, 3 and 6 all lead here.
| No. | Element | Shown when | Description · related API |
|---|---|---|---|
| ① | Video area | Always | Vertical video. Fill mode via setResizeMode (CENTER_CROP/FIT/AUTO) — Mission 7 |
| ② | Creator · brand | Always | Author and brand set in Shoplive Admin |
| ③ | Title · description · hashtags | Always | shortsDetail.title / description / tags |
| ④ | Product card · product banner | Always | A tap calls back into your app — Mission 5 (handleProductItem / getOnClickProductListener) |
| ⑤ | Like | Sign-in required | Hide with isLikeButtonVisible — Mission 7 |
| ⑥ | Comments | Sign-in required | isCommentButtonVisible |
| ⑦ | Bookmark | Sign-in required | isBookmarkVisible |
| ⑧ | Share | Always | A tap asks your app to present the share sheet — Mission 5 (handleShare / onShare) |
| ⑨ | Mute toggle | Always | Starts from isMuted; emits VIDEO_MUTED/VIDEO_UNMUTED |
| ⑩ | Close (back) | Always | To close it from your app, call ShopLiveShortform.close() |
Collections (embedded in your app)
A collection you place inside an existing screen. What each card shows is switched on and off with options (Mission 7).
| Type | Shape | Where it fits |
|---|---|---|
Card (CardType) | Grid with a configurable column count | Campaign or brand sections |
Vertical (Vertical) | A collection that continues downward | A whole tab dedicated to shorts |
Horizontal (Horizontal) | A carousel you swipe sideways | A strip in the middle of a home feed |
Full feed (DetailType / ShortsCollectionView) | The detail screen, as a view | Apps where a whole tab is shorts |
| Element in a card | Default | How to turn it off |
|---|---|---|
| Thumbnail / autoplaying video | Playing | disablePlayVideos() |
| Title | Shown | setVisibleTitle(false) |
| Description | Shown | setVisibleDescription(false) |
| Brand | Shown | setVisibleBrand(false) |
| Product count | Shown | setVisibleProductCount(false) |
| View count | Shown | setVisibleViewCount(false) |
Floating preview
A small window floating above another screen. Tapping it expands into the detail screen (Mission 3).
| Element | Description · related API |
|---|---|
| Position | One of the four corners — previewPosition / position (default bottomRight) |
| Size · corner radius | iOS previewScale · previewRadius / Android width · height · radius |
| Margins | iOS previewEdgeInset / Android marginTop and the other three sides |
| Close button | useCloseButton (shown by default) |
| Swipe to dismiss | iOS enableSwipeOut / Android setEnabledSwipeOut |
| Tap behavior | Expands to the detail screen by default. Your app can intercept it with useCustomAction + clickEventCallBack on iOS, or setUseCustomAction + setOnClickListener on Android |
Previous ← Quick StartNext → Try It in the Demo App
Try It in the Demo App
The demo app shipped with the SDK is organized around the same cases as this guide's missions (mission map). Seeing a behavior in the demo first makes the code much easier to follow.
- Default mode — Shoplive demo settings: right after install, every case runs against demo data with no input from you.
- My-info mode — enter client settings: enter your
accessKeyand, optionally, a user JWT in the settings screen to run the same cases against your shorts and your users. - Use the options dialog to change tags, brands, SKUs, shuffle, card type and preview position and watch how the options in Mission 7 show up on screen.
Previous ← Screen Terminology GuideNext → Mission 1 · Open shorts full screen
Mission 1 · Open shorts full screen
- Confirm
setAccessKeyruns at app startup (quick start). - In your entry button's action, build the query data and call
play. - Optionally attach a delegate or handler to receive product and share events — Mission 5.
@IBAction func openShorts() {
let data = ShopLiveShortformCollectionData(
tags: ["spring"],
tagSearchOperator: .AND,
shuffle: true,
delegate: self // optional: receive events
)
ShopLiveShortform.play(requestData: data)
}
fun openShorts() {
val data = ShopLiveShortformCollectionData().apply {
tags = listOf("spring")
tagSearchOperator = ShopLiveShortformTagSearchOperator.AND
shuffle = true
handler = myHandler // optional: receive events
}
ShopLiveShortform.play(context, data)
}
shortsId and that short opens first, with the rest following behind it.
This is what a push notification or deep link to a specific short should do.ShopLiveShortform.intent(context, data) returns an Intent.
Call startActivity yourself to control the transition and flags.Closing
ShopLiveShortform.close() // when your app must close it (sign-out, deep link, …)
ShopLiveShortform.close() // when your app must close it (sign-out, deep link, …)
Completion Checklist
- The video starts within a second or two of the tap
- Swiping up and down moves to the next and previous short
- Shorts that have products show a product card at the bottom (glossary ④)
- If a wrong query returns nothing, the empty state appears — check you didn't over-narrow it
Related: Events · UI optionsNext → Mission 2 · Embed a collection
Mission 2 · Embed a collection in your screen
Lists come in three shapes — card (grid), vertical and horizontal (carousel). All three take the same query and the same display options. You only pick the shape.
Step 1 · Create the view and attach it
// iOS — build it, then take the view out with getView()
private var builder: ShopLiveShortform.CardTypeViewBuilder?
override func viewDidLoad() {
super.viewDidLoad()
let builder = ShopLiveShortform.CardTypeViewBuilder()
let listView = builder.build(
cardViewType: .type1,
listViewDelegate: self,
shortsCollectionDelegate: self, // product and share events — Mission 5
enableSnap: false,
enablePlayVideo: true,
playOnlyOnWifi: false,
cellSpacing: 8,
cellRadius: 16,
viewCountVisibility: true
).getView()
listView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(listView)
// … Auto Layout constraints …
builder.setHashTags(tags: ["spring"], tagSearchOperator: .AND)
builder.submit() // ← required once the query is set
self.builder = builder
}
// Android — place the view in XML, configure it in code, then submit()
// res/layout/fragment_home.xml
// <cloud.shoplive.sdk.shorts.ShopLiveShortformCardTypeView
// android:id="@+id/shortsCardTypeView"
// android:layout_width="match_parent"
// android:layout_height="0dp" />
with(binding.shortsCardTypeView) {
setSpanCount(2) // grid columns
setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
handler = myHandler // product and share events — Mission 5
setHashTags(listOf("spring"), ShopLiveShortformTagSearchOperator.AND)
submit() // ← required once the query is set
}
listViewDelegate has two required methods (iOS)
ShopLiveShortformListViewDelegate must inherit NSObject, and both
onListViewError(error:) and onShortsSettingsInitialized() are non-optional.
Leave either one out and it will not compile. onShortsSettingsInitialized() is called only for card views built with CardTypeViewBuilder.
extension MyViewController: ShopLiveShortformListViewDelegate {
func onListViewError(error: Error) {
// The list query failed — show a retry, for example
}
func onShortsSettingsInitialized() {
// Server settings received — it is safe to re-apply options from here
}
}submit() and the collection stays empty
Query options (setHashTags, setBrands, setSkus, setShortsCollectionId)
do nothing on their own. The server query happens when you call submit().
The same applies when you change the query and reload (on iOS, reloadItems() also works).Step 2 · Pick the shape
// Vertical and horizontal come from ListViewBuilder
let builder = ShopLiveShortform.ListViewBuilder()
let listView = builder.build(
cardViewType: .type1,
listViewType: .horizontal, // .vertical | .horizontal
playableType: .CENTER, // which cell plays in a horizontal list
listViewDelegate: self,
shortsCollectionDelegate: self
).getView()
builder.submit()
// Vertical — ShopLiveShortformVerticalTypeView
with(binding.shortsVerticalTypeView) {
setSpanCount(2)
setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
submit()
}
// Horizontal — ShopLiveShortformHorizontalTypeView
with(binding.shortsHorizontalTypeView) {
setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
setPlayableType(ShopLiveShortform.PlayableType.CENTER)
enableSnap()
submit() // ← horizontal lists need submit() too
}
submit(collectionData, response) takes a response you already fetched.
Query it first with ShopLiveShortform.get(context, collectionData, listener) and pass it in —
handy when one query fills several carousels on a home screen.Step 3 · Stop playback when the screen goes away
Turning autoplay off when the screen isn't visible saves battery and data.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
builder?.enablePlayVideos()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
builder?.disablePlayVideos()
}
override func viewWillTransition(to size: CGSize, with c: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: c)
builder?.notifyViewRotated() // recalculate layout on rotation
}
override fun onResume() {
super.onResume()
binding.shortsCardTypeView.enablePlayVideos()
}
override fun onPause() {
super.onPause()
binding.shortsCardTypeView.disablePlayVideos()
}
Step 4 (optional) · Make a whole screen the shorts feed
Instead of presenting the full-screen feed, you can turn one of your tabs into it.
let feed = ShopLiveShortsCollectionView(
requestData: ShopLiveShortformCollectionData(tags: ["spring"], delegate: self)
)
view.addSubview(feed)
feed.resultHandler = { result in
if case .didScrollToShortsId(let shortsId) = result {
print("current short: \(shortsId ?? "")")
}
}
feed.action(.play) // .pause · .setMuted(true) · .setScrollEnabled(false) …
// res/layout — <cloud.shoplive.sdk.shorts.ShopLiveShortformDetailTypeView … />
with(binding.shortsDetailTypeView) {
setMuted(false)
setUIComponentPadding(bottom = bottomNavHeight) // keep clear of the bottom tab bar
setHashTags(listOf("spring"), ShopLiveShortformTagSearchOperator.AND)
submit() // ← always call once the query is set
}
// Forward the system back gesture to the feed
override fun onBackPressed() {
binding.shortsDetailTypeView.onBackPressed()
}
Completion Checklist
- The collection fills with thumbnails and visible cells start playing
- Tapping a card opens the full-screen detail
- Moving to another tab stops playback (
disablePlayVideos) - Changing the query and calling
submit()refills the collection
Related: Hiding card elementsNext → Mission 3 · Floating preview
Mission 3 · Show a floating preview
Previews connect products to shorts. Someone on a product page sees a small window with the short that features that product, and if they're interested it expands to full screen.
- When entering the product screen, pick related shorts with
productIdorskus. - Set the display options — position, size, close button.
- Dismiss the preview when the user leaves the screen.
let preview = ShopLiveShortformPreviewData(
productId: "PRODUCT_ID",
isMuted: true,
previewPosition: .bottomRight,
previewRadius: 12,
previewEdgeInset: .init(top: 0, left: 0, bottom: 24, right: 16),
useCloseButton: true,
enableSwipeOut: true,
maxCount: 10,
delegate: self
)
ShopLiveShortform.showPreview(requestData: preview)
// Dismissing
ShopLiveShortform.close()
ShopLiveShortform.hidePreview() // dismiss first so you never stack two
ShopLiveShortform.showPreview(
activity,
ShopLiveShortformPreviewData().apply {
productId = "PRODUCT_ID"
isMuted = true
setPosition(ShopLivePreviewPositionConfig.BOTTOM_RIGHT)
setRadius(12f)
setMarginBottom(24)
setMarginRight(16)
setUseCloseButton(true)
setIsAutoClosed(true)
maxCount = 10
handler = myHandler
}
)
// Dismissing
ShopLiveShortform.hidePreview()
// Is one on screen right now?
if (ShopLiveShortform.isShowingPreview()) { /* … */ }
hidePreview() on Android and close() on iOS.useCustomAction: true together with clickEventCallBack and a tap sends the user
wherever your app wants instead of expanding to full screen.Completion Checklist
- Entering the product page floats a small window in the corner you chose
- Tapping it expands to the full-screen detail
- The close button (or a swipe) dismisses it
- Navigating elsewhere leaves no preview behind
Related: Product tap eventsNext → Mission 4 · Connect user identity
Mission 4 · Connect user identity
setAuthToken and setUser only tell Shoplive who the current viewer is so that likes, comments, bookmarks and viewing history are attributed to that member. Shoplive does not check the user's credentials. With a JWT (setAuthToken), Shoplive can verify that the information was issued by your server and not altered (signature check); with a profile (setUser), the information is passed as is, without that check.Step 1 · Do you need it? — how to decide
| Situation | Needed? |
|---|---|
| Users only watch (playback, scrolling, product taps) | No — works signed out |
| Users like, comment or bookmark | Yes — without identity nothing is recorded |
| You need per-user history or personalization | Yes |
Step 2 · Choose by sign-in state — recommended setup
| User | Recommended | Why |
|---|---|---|
| Signed out | Call nothing | The server issues the guest identifier. To turn a signed-in user back into a guest, reset the user info with the sign-out calls below |
| Signed in (recommended) | setAuthToken — a JWT signed by your server | It can't be forged, so you can trust that likes, comments, bookmarks and viewing history belong to that member |
| Signed in (when backend work is hard) | setUser — pass the profile (userId, name, and so on) | Unsigned, client-supplied values that can be forged. It's enough for identification and stats, but don't use it for authorization |
| Log out · reset user info | setAuthToken(nil/null) + setUser(nil/null) (iOS and Android) | Clears the user info you set (JWT and profile) so the previous user's activity doesn't bleed through. The API is named setAuthToken, but the value you pass is user info (a JWT) |
setAuthToken → on sign-out, reset the user info.// ① Signed in · secure auth (recommended) — a JWT signed by your backend (Step 4)
ShopLiveCommon.setAuthToken(authToken: myJwtFromServer)
// ② Signed in · simple — pass the profile directly (unsigned · not for authorization)
ShopLiveCommon.setUser(user: ShopLiveCommonUser(
userId: hashedUserId, // a hash, not the raw member ID (Step 3)
userName: "Jane Doe",
age: 29,
gender: .female,
userScore: 3
))
// ③ Signed out — no call needed
// Sign-out — reset the user info (clears the JWT and profile)
ShopLiveCommon.setAuthToken(authToken: nil)
ShopLiveCommon.setUser(user: nil)
// Current state
let isLoggedIn = ShopLiveCommon.isLoggedIn()
// ① Signed in · secure auth (recommended) — a JWT signed by your backend (Step 4)
ShopLiveCommon.setAuthToken(myJwtFromServer)
// ② Signed in · simple — pass the profile directly (unsigned · not for authorization)
ShopLiveCommon.setUser(
ShopLiveCommonUser(userId = hashedUserId).apply { // a hash, not the raw member ID (Step 3)
setUserName("Jane Doe")
setAge(29)
setGender(ShopLiveCommonUserGender.FEMALE)
}
)
// ③ Signed out — no call needed
// Sign-out — reset the user info (clears the JWT and profile)
ShopLiveCommon.setAuthToken(null)
ShopLiveCommon.setUser(null)
clearAuth() clears a different scope on each platform On Android it clears only the user info (user, JWT, userId). On iOS it resets the whole common state, including your accessKey, guest identifier and UTM, so if you use it, set the accessKey again before the next use. To clear only the user info, pass nil (null) to setAuthToken and setUser as shown above.Step 3 · ID hashing — don't expose raw member IDs
userId is stored on Shoplive servers and in Shoplive Admin, and can be visible to other users.
Hash your internal member ID before passing it, and keep a mapping table so your app can go back the other way.Use a value that your app and backend hash the same way as the userId — in the JWT and in the profile alike.
The key is using a consistent salt and algorithm so the same user always produces the same value (to keep likes and bookmarks, and for statistics and return-visitor identification).
View hashing code samples (Swift · Kotlin)
import CryptoKit
func hashUserId(_ raw: String) -> String {
let salted = raw + "APP_SALT"
let digest = SHA256.hash(data: Data(salted.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
import java.security.MessageDigest
fun hashUserId(raw: String): String {
val salted = raw + "APP_SALT"
return MessageDigest.getInstance("SHA-256")
.digest(salted.toByteArray())
.joinToString("") { "%02x".format(it) }
}
setAuthToken). We recommend agreeing on the salt policy with the Shoplive team.Step 4 · Create the JWT for secure auth (your backend)
The JWT you pass to setAuthToken is signed by your backend with the Secret Key that Shoplive issues. The app only fetches the JWT from your backend and passes it to setAuthToken.
It is the same JWT as the Live (Player) SDK.
- Get the Secret Key from your Shoplive contact. It's a base64-encoded string
- Build the payload below from the signed-in user and sign it with HS256
- Issue a new JWT every time the app asks (right before each visit to a shorts screen). Don't store a JWT in the app and reuse it
exp. Have the app request a new JWT from your backend whenever it calls setAuthToken.| claim | Required | Description | Example |
|---|---|---|---|
exp | Required | Expiration time (Unix seconds) | 1516239022 |
userId | Required | Unique value for the signed-in user (up to 255 bytes). A hashed value works too (Step 3) | sample_user_id |
iat | Issued-at time (Unix seconds) | 1516195822 | |
name | Name shown in chat. Banned words are masked with asterisks (*) | LiveFan | |
gender | Gender — m · f · empty (unspecified) | f | |
age | Age. Stats group it in 5-year bands | 27 | |
userScore | User grade (-100 to 100). The basis for grade-based entry and prize-draw limits | 10 | |
userType | User type. Lets only certain types into broadcasts with an entry restriction (Entry Type) | VIP | |
profile | Chat profile image URL (64×64px recommended) | https://…/profile.png | |
custom | Arbitrary data (up to 1 KB) | {"grade":"vip"} |
Signing examples. Both produce the same token — just note that the Secret Key is a base64 string.
// Node.js — npm i jsonwebtoken
const jwt = require("jsonwebtoken");
const secret = Buffer.from(process.env.SHOPLIVE_SECRET_KEY, "base64"); // base64-decode it into bytes
const token = jwt.sign(
{ userId: hashedUserId, name: "LiveFan", gender: "f", age: 27, userScore: 10 },
secret,
{ algorithm: "HS256", expiresIn: "12h" } // fills in exp and iat automatically
);
// Java — io.jsonwebtoken (jjwt)
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey) // pass the base64 string as is
.setExpiration(new Date(System.currentTimeMillis() + 12 * 60 * 60 * 1000L))
.setIssuedAt(new Date())
.claim("userId", hashedUserId)
.claim("name", "LiveFan")
.claim("gender", "f")
.claim("age", 27)
.claim("userScore", 10)
.compact();
setAuthToken. A profile (setUser) is unsigned, so you can't use it for authorization.Step 5 · When to call it
- Right after sign-in — before the user reaches a shorts screen
- Each time the user enters a shorts screen — call
setAuthTokenagain with a newly issued JWT from your backend (don't reuse a stored token) - On sign-out — reset the user info so the previous user's activity doesn't bleed through
Completion Checklist
- After signing in, a like survives an app restart
- After signing in, writing a comment isn't blocked
- After sign-out → resetting the user info, likes and bookmarks reset when you come back (you watch as a guest)
Previous ← Mission 3Next → Mission 5 · Events, products, sharing
Mission 5 · Events, products, sharing
The SDK draws the screen and leaves the decisions to you. Where a product tap goes and what link sharing produces are both your app's calls. The delegate (iOS) / handler (Android) is how they reach you.
Delegate surface — three groups
| What | iOS | Android |
|---|---|---|
| Product tap | handleProductItem(shortsId:shortsSrn:product:) | getOnClickProductListener() |
| Product banner tap | handleProductBanner(shortsId:shortsSrn:scheme:) | getOnClickBannerListener() |
| Share tap | handleShare(shareMetadata:) · handleShare(shareUrl:) | onShare(context, data) · onShare(context, url) |
| General events | onEvent(messenger:command:payload:) | onEvent(context, messenger, command, payload) |
| Errors | onError(error:) | onError(context, error) |
| Appear / disappear | onDidAppear() · onDidDisAppear() | onCreate() · onDestroy() |
| Short changed | onShortsAttached/Detached(data:) | onShortsAttached/Detached(data) |
Step 1 · Wire it up
// Put the delegate on the data object (same for playback, preview and lists)
let data = ShopLiveShortformCollectionData(delegate: self)
ShopLiveShortform.play(requestData: data)
extension MyViewController: ShopLiveShortformReceiveHandlerDelegate {
// Implement only what you need — every method is optional
}
// Declare the handler first
val myHandler = object : ShopLiveShortformHandler() {
// Override only what you need
}
// ① Per data object / per view — this is the normal way
val data = ShopLiveShortformCollectionData().apply { handler = myHandler }
binding.shortsCardTypeView.handler = myHandler
// ② Global — only read on the hybrid (WebView) bridge path
ShopLiveShortform.setHandler(myHandler)
Step 2 · Product tap → your detail page
func handleProductItem(shortsId: String, shortsSrn: String, product: ProductData) {
// Route on whichever key your app uses: url · sku · productId · customerProductId
guard let urlStr = product.url, let url = URL(string: urlStr) else { return }
navigationController?.pushViewController(ProductDetailViewController(url: url), animated: true)
}
func handleProductBanner(shortsId: String, shortsSrn: String, scheme: String) {
// Banners hand you the scheme or URL configured in the console, as-is
openDeepLink(scheme)
}
override fun getOnClickProductListener() = ShopLiveShortformProductListener { context, data, product ->
// Route on whichever key your app uses: product.url · sku · productId
product.url?.takeIf { it.startsWith("http") }?.let { url ->
ProductDetailActivity.start(context, url)
}
}
override fun getOnClickBannerListener() = ShopLiveShortformUrlListener { context, data, url ->
openDeepLink(context, url)
}
customerProductId
productId is Shoplive's internal identifier. To reach your own product page, route on sku or url.On iOS the callback also carries the merchant product ID (
customerProductId), but Android's product callback model has no such field.Step 3 · Share tap → your share sheet
On a share tap the SDK calls back into your app. If Shoplive Admin or the web layer supplies a share URL you get
handleShare(shareUrl:); otherwise you get handleShare(shareMetadata:).
Your app builds its own deep link or dynamic link and presents the share sheet.
handleShare(shareUrl:) unimplemented and the SDK presents its own sheet
If you implement only the shareMetadata overload, shares that arrive through the URL path make
the SDK present the default iOS share sheet instead. To keep sharing entirely yours, implement both overloads.func handleShare(shareMetadata: ShopLiveShareMetaData) {
// title · descriptions · thumbnail · shortsId · shortsSrn
let link = makeMyDeepLink(shortsId: shareMetadata.shortsId)
let vc = UIActivityViewController(activityItems: [shareMetadata.title ?? "", link],
applicationActivities: nil)
// Shorts live in their own window — present relative to that window
ShopLiveShortform.getCurrentKeyWindow()?.rootViewController?.present(vc, animated: true)
}
override fun onShare(context: Context, data: ShopLiveShortformShareData) {
// shortsId · srn · title · description · thumbnail
val link = makeMyDeepLink(data.shortsId)
val send = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "${data.title}\n$link")
}
context.startActivity(Intent.createChooser(send, null))
}
ShopLiveShortform.getCurrentKeyWindow().Step 4 · Receiving general events
func onEvent(messenger: ShopLiveShortformMessenger?, command: String, payload: String?) {
switch command {
case "VIDEO_MUTED": isMuted = true
case "VIDEO_UNMUTED": isMuted = false
case "DETAIL_ON_PLAYER_SHOWN": analytics.log("shorts_detail_open")
case "COLLECTION_CLICK_ITEM": analytics.log("shorts_card_click")
default: break // ignore commands you don't know
}
}
override fun onEvent(
context: Context,
messenger: ShopLiveShortformMessageListener?,
command: String,
payload: Map<String, Any?>
) {
when (command) {
"VIDEO_MUTED" -> isMuted = true
"VIDEO_UNMUTED" -> isMuted = false
else -> Unit // ignore commands you don't know
}
}
default
New event commands appear in SDK updates. Handle the ones you know and ignore the rest,
and upgrading the SDK will never break your app. The list is in Types & States.Completion Checklist
- Tapping a product card opens your app's product detail
- Tapping share opens a share sheet with your link, on top of the shorts screen
- Toggling mute logs
VIDEO_MUTED/VIDEO_UNMUTED - Failures surface with a
code/messageinonError(error handling)
Previous ← Mission 4Next → Mission 6 · Play your own list
Mission 6 · Play your own list
The query options in Mission 1 let Shoplive decide the list. When your server has to decide the order — a recommendation feed, for example — pass the shorts IDs directly.
- Fetch the first page of shorts IDs from your API.
- Pass the IDs and a starting point (
currentId) to play. - When the SDK reaches the end it asks for the next page — your app fetches and fills it.
// 1 and 2) Play the first page
let ids = firstPage.map { ShopLiveShortformIdData(shortsId: $0.id, payload: nil) }
ShopLiveShortform.play(
shortformIdsData: ShopLiveShortformIdsData(ids: ids, currentId: ids.first?.shortsId),
dataSourceDelegate: self,
shortsCollectionDelegate: self
)
// ③ Pagination — downward and upward, separately
extension MyFeed: ShortsCollectionViewDataSourcRequestDelegate {
func onShortformListDownwardPagination(
completion: @escaping ((ShopLiveShortformIdsMoreData?, Error?)) -> ()
) {
myAPI.next { page, error in
guard let page = page else { completion((nil, error)); return }
let more = ShopLiveShortformIdsMoreData(
ids: page.items.map { ShopLiveShortformIdData(shortsId: $0.id, payload: nil) },
hasMore: page.hasMore
)
completion((more, nil))
}
}
func onShortformListUpwardPagingation(
completion: @escaping ((ShopLiveShortformIdsMoreData?, Error?)) -> ()
) {
completion((nil, nil)) // nil when there is nothing above
}
}
// 1, 2 and 3) Playback plus pagination (coroutine flavor)
val idsData = ShopLiveShortformIdsData().apply {
ids = firstPage.map { ShopLiveShortformIdData(it.id) }
currentId = firstPage.firstOrNull()?.id
handler = myHandler
}
ShopLiveShortform.play(
context,
idsData,
moreSuspendListener = { // load more, downward
val page = myApi.next()
ShopLiveShortformIdsMoreData().apply {
ids = page.items.map { ShopLiveShortformIdData(it.id) }
hasMore = page.hasMore
}
},
previousSuspendListener = { null } // null when there is nothing above
)
// Not using coroutines? There is a Future variant too
// ShopLiveShortform.play(context, idsData, moreFutureListener, previousFutureListener)
hasMore = false at the end
If the last page doesn't set hasMore to false, the SDK keeps asking for more pages.
Signal the end with an empty array plus hasMore=false.ShopLiveShortformIdData.payload
and you get it back in the event callbacks for logging.Completion Checklist
- Playback follows exactly the order your server sent
- The short named by
currentIdis the first screen - Scrolling to the end appends the next page
- After the last page, requests stop (
hasMore=false)
Previous ← Mission 5Next → Mission 7 · Customize the UI
Mission 7 · Customize the UI
Option ① · Hide detail-screen buttons
Like, comment, bookmark and share can each be turned off. Hiding what your service doesn't offer avoids confusion.
ShopLiveShortform.setVisibileDetailViews(options: ShopLiveShortformVisibleDetailData(
isBookMarkVisible: false,
isShareButtonVisible: true,
isCommentButtonVisible: false,
isLikeButtonVisible: true
))
ShopLiveShortform.setVisibleDetailTypeViews(
ShopLiveShortformVisibleDetailTypeData(
isBookmarkVisible = false,
isShareButtonVisible = true,
isCommentButtonVisible = false,
isLikeButtonVisible = true
)
)
play or showPreview
for it to take effect on that screen.Option ② · Video fill mode
ShopLiveShortform.setResizeMode(mode: .CENTER_CROP) // .FIT | .AUTO
ShopLiveShortform.setResizeMode(ShopLiveResizeMode.CENTER_CROP) // FIT | AUTO
| Value | Behavior | Where it fits |
|---|---|---|
CENTER_CROP | Fills the screen, cropping what doesn't fit | Services with vertical video only |
FIT | Fits the whole frame, letterboxing as needed | A mix of landscape and square video |
AUTO | The SDK picks based on the video's ratio | Default · mixed aspect ratios |
Option ③ · What a collection card shows
builder.setVisibleTitle(isVisisble: true)
builder.setVisibleDescription(isVisible: false)
builder.setVisibleBrand(isVisible: true)
builder.setVisibleProductCount(isVisible: true)
builder.setVisibleViewCount(isVisible: false)
builder.setCellSpacing(spacing: 8)
builder.setCellCornerRadius(radius: 16)
builder.setCellBackgroundColor(color: .white)
builder.setCardViewType(type: .type2)
builder.reloadItems() // apply the changes
with(binding.shortsCardTypeView) {
setVisibleTitle(true)
setVisibleDescription(false)
setVisibleBrand(true)
setVisibleProductCount(true)
setVisibleViewCount(false)
setRadius(16f.toPx(context))
setCardBackgroundColor(Color.WHITE)
setViewType(ShopLiveShortform.CardViewType.CARD_TYPE2)
submit() // apply the changes
}
Option ④ · Platform-specific settings
| Option | Platform | Description |
|---|---|---|
setSystemBarColor(data) | Android | Match the detail screen's status and navigation bars to your theme |
setSoftInputMode(mode) | Android | Keyboard behavior while commenting (ADJUST_RESIZE by default) |
setMaxCount(n) | Android (collections) | Cap how many items a collection shows |
setEnableResumeOnForeGround(enable:) | iOS | Whether playback resumes when the app returns to the foreground |
avAudioSessionCategoryOptions | iOS (builder) | How audio mixes with other apps |
setPlayOnlyWifi(true) | Both | Autoplay collections on Wi-Fi only — considerate of data-conscious users |
Completion Checklist
- Buttons you turned off aren't on the detail screen
- Video fills the way you intended (cropped or fully visible)
- Cards show only the elements you kept
- On Android the status bar color matches your theme
Previous ← Mission 6Next → Appendix · Extra features
Extra Features
Things outside the missions, for when you need them.
Fetching collection data only (Android)
Use this to get data without showing a screen — to draw it yourself, or to fill several horizontal carousels at once.
ShopLiveShortform.get(context, collectionData, object : ShopLiveShortformCollectionListener {
override fun onData(data: ShopLiveShortformCollectionResponse) {
binding.shortsHorizontalTypeView.submit(collectionData, data)
}
override fun onError(e: Exception) { /* … */ }
})
// iOS has no equivalent public API.
// If you only need the data, query it from your server with the Shoplive Open API
// and feed the IDs in through Mission 6 (play your own list).
get() needs a LifecycleOwner
It runs on lifecycleScope internally, so the context has to be an Activity or Fragment.
Anything else and nothing happens, silently. Don't pass applicationContext.Video cache control (iOS)
Shorts cache the videos they play on disk. In a long-lived app that adds up, so if your settings screen has a "clear cache" action, clear this along with it.
// Clear what is cached right now
AVAssetDownloadManager.shared.deleteCaches()
// Or have it cleared automatically on app termination (call once at startup)
AVAssetDownloadManager.shared.enableDeleteCacheOnTerminate(isEnabled: true)
// Android exposes no equivalent public cache-control API.
// It follows the playback engine's cache policy (media3 / ExoPlayer).
Attribution (referrer and UTM)
Record which screen sent the user into shorts so you can analyze conversion.
ShopLiveShortformCollectionData, ShopLiveShortformPreviewData and
ShopLiveShortformIdsData overwrite the global value even when you omit referrer — with nil.
A referrer set earlier through setReferrer disappears the moment you build one of these,
so always pass referrer: in the initializer on screens that use a data object.ShopLiveShortform.setReferrer(referrer: "home_banner") // global
// Or at construction: ShopLiveShortformCollectionData(referrer: "home_banner", …)
ShopLiveCommon.setUtmSource(utmSource: "naver")
ShopLiveCommon.setUtmMedium(utmMedium: "cpc")
ShopLiveCommon.setUtmCampaign(utmCampaign: "spring_sale")
ShopLiveCommon.setUtmContent(utmContent: "banner_a")
val data = ShopLiveShortformCollectionData().apply { referrer = "home_banner" }
// List views: binding.shortsCardTypeView.setReferrer("home_tab")
ShopLiveCommon.setUtmSource("naver")
ShopLiveCommon.setUtmMedium("cpc")
ShopLiveCommon.setUtmCampaign("spring_sale")
ShopLiveCommon.setUtmContent("banner_a")
Hybrid (WebView) integration
For showing shorts inside a web view in your app: the native SDK receives the shorts calls the page makes.
// Connect right after creating the web view
ShopLiveShortform.BridgeInterface.connect(myWebView)
// To show an app screen (e.g. product detail) on top of the shorts full screen
ShopLiveShortform.BridgeInterface.present(viewController: productDetailVC)
// When leaving the screen
ShopLiveShortform.BridgeInterface.disconnect()
// Right after creating the web view, register the bridge as a JavaScript interface
val bridge = ShopLiveShortformBridgeInterface(myWebView, null)
myWebView.addJavascriptInterface(bridge, "ShopLiveBridgeInterface")
bridge.observe(activity) // receive messages for the ComponentActivity's lifetime
// When leaving the screen
bridge.removeObserver()
present(viewController:) is not for connecting (iOS)
Connect with connect · disconnect. present(viewController:) shows the screen you pass on top of the shorts full screen (overFullScreen).Data Field Dictionary
Query options — choosing which shorts to show
These mean the same thing in playback, collections and previews.
| Field | Type | Meaning |
|---|---|---|
shortsId | String | Open this short first (used for deep links) |
shortsCollectionId | String | Query by a collection created in Shoplive Admin |
tags | String[] | Filter by hashtag |
tagSearchOperator | AND · OR | AND = match every tag · OR = match any (types) |
brands | String[] | Filter by brand identifier |
skus | String[] | Only shorts linked to these SKUs |
productId | String | Shorts related to a product (mostly for previews) |
shuffle | Bool | Randomize the order so it differs each time |
isMuted | Bool | Start muted |
referrer | String | Record the entry point (appendix) |
tags and brands together returns only shorts that satisfy both.
If you get nothing back, remove conditions one at a time.ShopLiveShortformCollectionData — playback and collection queries
| Aspect | iOS | Android |
|---|---|---|
| Construction | ShopLiveShortformCollectionData(…, delegate:) — init arguments | ShopLiveShortformCollectionData().apply { … } — properties |
| Receiving events | delegate | handler |
| Query options | Same as the query options table above | |
ShopLiveShortformPreviewData — floating preview
The query options plus display options.
| Option | iOS | Android | Default |
|---|---|---|---|
| Position | previewPosition | setPosition(…) | bottomRight |
| Size | previewScale | setWidth · setHeight | iOS derived from screen width · Android long side 180dp |
| Corner radius | previewRadius | setRadius | 10 |
| Outer margin | previewEdgeInset | setMargin{Top,Bottom,Left,Right} | 16 |
| Close button | useCloseButton | setUseCloseButton | true |
| Swipe to dismiss | enableSwipeOut | setEnabledSwipeOut | true |
| Max items | maxCount | maxCount | Unlimited |
| Intercept tap | useCustomAction + clickEventCallBack | setUseCustomAction + setOnClickListener | false |
| Haptic on tap | — | setCanVibrated | — |
| Volume keys | isEnabledVolumeKey | isEnabledVolumeKey | false |
| Auto dismiss | — | setIsAutoClosed | — |
| Transition animation | — | setCanTransition | — |
setWidth · setHeight · setMargin… · setRadius. The defaults (long side 180 · margin 16 · radius 10) are in dp and are converted to px for the device density.ShopLiveShortformIdsData — playing your own list
| Field | Type | Meaning |
|---|---|---|
ids | ShopLiveShortformIdData[] | Shorts IDs to play (array order = playback order) |
currentId | String | Which short opens first |
isMuted | Bool | Start muted (Android) |
referrer | String | Entry point |
| ShopLiveShortformIdData / IdsMoreData | Meaning |
|---|---|
IdData.shortsId | Shorts ID |
IdData.payload | Arbitrary data your app carries along (reason, ranking, …) |
IdsMoreData.ids | Next page of IDs to append |
IdsMoreData.hasMore | false stops the SDK from asking for more |
Collection options
| Option | iOS (builder) | Android (view) |
|---|---|---|
| Card shape | setCardViewType(type:) | setViewType(type) |
| Column count | — | setSpanCount(n) |
| Snap scrolling | enableSnap() / disableSnap() | enableSnap() / disableSnap() |
| Autoplay | enablePlayVideos() / disable… | enablePlayVideos() / disable… |
| Wi-Fi only | setPlayOnlyWifi(isEnabled:) | setPlayOnlyWifi(isEnabled) |
| Cell spacing | setCellSpacing(spacing:) | Layout / item decoration |
| Corner radius | setCellCornerRadius(radius:) | setRadius(radius) |
| Background color | setCellBackgroundColor(color:) | setCardBackgroundColor(colorInt) |
| Visible elements | setVisibleTitle/Description/Brand/ProductCount/ViewCount | Same |
| What plays (horizontal) | setPlayableType(type:) | setPlayableType(type) |
| Max items | — | setMaxCount(n) |
| Scroll to top | scrollToTop() | scrollToTop(isSmooth) |
| Apply the query | submit() · reloadItems() | submit() |
Detail-screen visibility
| Field | Default | Description |
|---|---|---|
isLikeButtonVisible | true | Like — needs identity |
isCommentButtonVisible | true | Comments — needs identity |
isBookMarkVisible (Android: isBookmarkVisible) | true | Bookmark — needs identity |
isShareButtonVisible | true | Share — handled by your app |
User identity (ShopLiveCommonUser)
| Field | Required | Description |
|---|---|---|
userId | ✔ | Member identifier — pass a hash, not the raw ID (Mission 4) |
userName | Display name | |
age | Age | |
gender | iOS .male/.female/.netural · Android MALE/FEMALE/NEUTRAL | |
userScore | Tier or score, used for personalization | |
custom | Any extra values your app wants to carry |
ProductData — what a product callback gives you
| Field | Description |
|---|---|
productId | Shoplive's internal product ID |
customerProductId | Your product ID — iOS only (absent from the Android callback model) |
sku | Stock keeping unit |
url | Product URL registered in Shoplive Admin |
name · brand · descriptions | Display information |
imageUrl | Product image |
currency · originalPrice · discountPrice · discountRate | Pricing |
showPrice | Whether to show the price (console setting) |
stockStatus | Stock state |
Share metadata
| Field | Description |
|---|---|
shortsId · shortsSrn (Android srn) | Shorts identifiers — what to put in your deep link |
title · description | Title and description for the share text |
thumbnail | Thumbnail URL for the OG image |
Types & States
Card and collection types
| Type | iOS | Android |
|---|---|---|
| Card shape | CardViewType.type0 / type1 / type2 | CardViewType.CARD_TYPE0 / 1 / 2 |
| Collection direction | ListViewType.vertical / horizontal | Separate view classes (Vertical / Horizontal) |
PlayableType — what plays in a collection
| Value | Behavior | Where it fits |
|---|---|---|
FIRST | Only the first cell plays (default) | Saving battery and data |
CENTER | The centered cell plays | Horizontal carousels |
ALL | Every visible cell plays | Maximum impact — watch performance |
PreviewPosition
| iOS | Android |
|---|---|
.topLeft · .topRight · .bottomLeft · .bottomRight · .default |
TOP_LEFT · TOP_RIGHT · BOTTOM_LEFT · BOTTOM_RIGHT |
.default is the same as bottomRight.ShopLiveResizeMode
| Value | Description |
|---|---|
CENTER_CROP | Fill the screen, cropping the overflow |
FIT | Fit the whole frame |
AUTO | Chosen from the video's ratio (default) |
TagSearchOperator
| Value | Meaning |
|---|---|
AND | Shorts carrying all of the given tags |
OR | Shorts carrying any of them |
Event commands
What arrives in onEvent (Mission 5). This list can grow, so handle
what you know and ignore the rest.
| Command | When |
|---|---|
VIDEO_MUTED / VIDEO_UNMUTED | Mute toggled |
DETAIL_ON_PLAYER_SHOWN (iOS)DETAIL_ON_PLAYER_SHOW (Android) | Entered the full-screen detail — the string differs by platform (no trailing N on Android) |
DETAIL_EMPTY | The query returned nothing — too narrow, or no matching shorts in Shoplive Admin |
DETAIL_ON_PLAYER_DISMISS | Left the detail |
COLLECTION_SHOW | A collection became visible |
COLLECTION_CLICK_ITEM | A collection card was tapped |
PREVIEW_SHOWN / PREVIEW_HIDDEN | Preview appeared / disappeared |
PREVIEW_CLICK_SHOW | Preview tapped and expanded |
PREVIEW_CLICK_CLOSE | Preview close button tapped |
String?); Android delivers a Map (Map<String, Any?>).
If you want shared logic, normalize to one shape in your app.Error Handling
Errors arrive through onError (delegate / handler) and, on iOS, the collection-specific
onListViewError. Both platforms use the same code scheme.
Code ranges
| Range | Meaning |
|---|---|
| 9000s | Initialization and setup problems — something your app must fix |
| 9300s | Encoding — mostly the Editor SDK |
| 9500s | Unsupported media or feature |
| 9900s | Network and parsing |
| 10000 | Unclassified exception |
Errors you'll actually meet
| Code | Name | Cause · fix |
|---|---|---|
9000 | NOT_INITIALIZED_ACCESS_KEY | The SDK was called before setAccessKey → move it to Application.onCreate/AppDelegate |
9001 | NOT_INITIALIZED_SHARE_URL | No share URL configured → check Shoplive Admin |
9002 | NOT_INITIALIZED_CONFIG | Shorts configuration not received yet → check the network and retry |
9050 | REQUIRED_COMPONENT_ACTIVITY | (Android) A non-ComponentActivity context was passed → pass the activity, not applicationContext |
9310 | REMOVED_VIDEO | A deleted or private short was opened → validate the shortsId in your deep link |
9501 | UNSUPPORTED_MEDIA | A codec the device can't play |
9900 | FAILED_NETWORK | Network failure → offer a retry |
9901 | FAILED_JSON_PARSING | Response parsing failed → suspect an SDK/server version mismatch |
9902 | FAILED_FETCH_DATA | (Android) Query failed → check the query options and permissions |
10000 | UNEXPECTED_ERROR | Unclassified → contact us with the logs |
func onError(error: Error) {
guard let e = error as? ShopLiveCommonError else { return }
log("shortform error \(e.codes) \(e.message ?? "")")
// 9900s → offer a retry; 9000s → assert during development
}
override fun onError(context: Context, error: ShopLiveCommonError) {
log("shortform error ${error.code} ${error.message}")
when (error.code) {
ShopLiveCommonErrorCode.FAILED_NETWORK -> showRetry()
ShopLiveCommonErrorCode.NOT_INITIALIZED_ACCESS_KEY -> check(false) { "access key not set" }
else -> Unit
}
}
onError makes a failing collection look like an empty collection, with no way to tell why.
Log it at minimum, and make it visible (a toast, say) in development builds.Client-Configured Items
These live in Shoplive Admin or your backend, not in code. When they're missing, correct code still shows an empty screen.
| Item | Where | Why it matters |
|---|---|---|
| accessKey | Issued by Shoplive | Required by every call (quick start) |
| Shorts content registered | Shoplive Admin | Without video, thumbnail and title there is nothing to list |
| Tags · brands · collections | Shoplive Admin | Must match your query strings exactly |
| Product links (SKU, product URL) | Shoplive Admin | Drives the product card and routing |
| Share URL | Shoplive Admin | Missing it produces 9001 |
| JWT secret | Your Shoplive contact → your backend | Connect user identity. Never ship it in the app |
| Deep link domain | Your side | So shared links come back into the app |
spring-new in Shoplive Admin and query spring new and you get nothing.
Keep them in shared constants so both sides use the same value.Troubleshooting & FAQ
The collection is empty
In order: ① did you call submit() (Mission 2) ② is the query too narrow —
tags, brands and SKUs combine with AND ③ does Shoplive Admin actually have shorts matching it
(configuration) ④ what does onError report?
Only thumbnails show, no video (Android)
Most likely the playback engine module (shoplive-media3 or shoplive-exoplayer) isn't in your
dependencies. The shortform module references it as compileOnly, so your app must declare it
(install).
Likes don't stick between sessions
There is no user identity. Call setAuthToken or setUser
before the user reaches a shorts screen (Mission 4).
The share sheet is hidden behind the shorts screen (iOS)
The full-screen feed lives in the SDK's own UIWindow. Present relative to the window from
ShopLiveShortform.getCurrentKeyWindow() (Mission 5).
The preview stays on screen after I navigate away
The preview is a separate layer floating above the app, so navigation doesn't remove it.
Call hidePreview() (Android) or close() (iOS) when leaving (Mission 3).
Scrolling to the end keeps requesting more pages
The last page didn't return hasMore = false (Mission 6).
Audio keeps playing after I switch tabs
Collection autoplay was left on when the screen went away. Call disablePlayVideos() in
onPause/viewDidDisappear (Mission 2).
I want users to create and upload shorts
That's outside this SDK. See the Shortform Editor SDK guide.
Can I use this alongside the Live (Player) SDK?
Yes. The accessKey and user identity are set once in the shared module and both SDKs use them.
Just keep the two SDKs on matching versions.