@bambuser/react-native-commerce-sdk
v2.1.0
Published
React Native wrapper for Bambuser Commerce SDK
Readme
@bambuser/react-native-commerce-sdk
React Native wrapper for the Bambuser Commerce SDK. Embed live video shopping experiences in your iOS and Android React Native apps with a single declarative component.
The package wraps Bambuser's native iOS and Android SDKs (versions 3.3.0 and 3.7.0 respectively) and exposes them through a React component plus an imperative ref-based API. No native code required in your app.
Feature Overview
| Feature | iOS | Android |
|---|---|---|
| Live video playback | ✅ | ✅ |
| Play / pause / mute / unmute | ✅ | ✅ |
| invoke / notify (call into player & respond to callbacks) | ✅ | ✅ |
| Picture-in-Picture | ✅ | ✅ |
| Player state events (onStatus, onProgress, onError) | ✅ | ✅ |
| Custom events (onEvent) | ✅ | ✅ |
| Thumbnail tap callback (onThumbnailTapped) | ✅ | – |
| Safe-area edge control | ✅ | ✅ |
| Video scale mode (videoScaleMode) | ✅ | ✅ |
| Analytics tracking (BambuserSDK.track) | ✅ | ✅ |
| Shoppable video playback (mode="shoppable") | ✅ | ✅ |
| Shoppable collection lookup (fetchShoppableCollection) | ✅ | ✅ |
| Shoppable collection metadata (fetchShoppableCollectionMetadata) | ✅ | ✅ |
| Preview / full-experience mode switch (setMode) | ✅ | ✅ |
| Reset shoppable player (resetPlayer) | ✅ | – |
| Seek (seek) | ✅ | ✅ |
Requirements
- React Native: >= 0.72
- iOS: >= 15.6
- Android:
minSdkVersion >= 26
Installation
npm install @bambuser/react-native-commerce-sdk
# or
yarn add @bambuser/react-native-commerce-sdkiOS
cd ios && pod installThe Bambuser Commerce SDK xcframework is vendored inside the package — no extra source/repo configuration required.
Add -ObjC to your app target's Other Linker Flags (Build Settings → Linking). Without it, Objective-C categories inside the SDK are stripped at link time and the player crashes at runtime.
Android
The Android SDK is published to a Bambuser-hosted Maven repo. Add it to your project's android/settings.gradle:
dependencyResolutionManagement {
repositories {
// ... existing repos
maven { url "https://repo.repsy.io/mvn/bambuser/bambuser-commerce-sdk" }
}
}The SDK uses Jetpack Compose internally. Add the Compose Kotlin plugin to your root android/build.gradle:
plugins {
id "org.jetbrains.kotlin.plugin.compose" version "2.1.20" apply false
}Bump minSdkVersion to 26 or higher in your android/build.gradle.
Picture-in-Picture (Android)
Android uses activity-level PiP. To enable it, add supportsPictureInPicture to your host Activity in AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
... />When the activity enters PiP, the entire activity shrinks (not just the video). Hide non-video UI when onPiPStateChanged fires 'started' and restore it on 'stopped'.
Quick Start
import { useRef } from 'react';
import {
BambuserVideoView,
type BambuserVideoViewRef,
} from '@bambuser/react-native-commerce-sdk';
export function LiveVideoScreen() {
const playerRef = useRef<BambuserVideoViewRef>(null);
return (
<BambuserVideoView
ref={playerRef}
style={{ flex: 1 }}
id="your-video-id"
server="US"
configuration={{
autoplay: true,
currency: 'USD',
locale: 'en-US',
}}
onStatus={(e) => console.log('state:', e.nativeEvent.state)}
onError={(e) => console.error(e.nativeEvent.message)}
/>
);
}Component API
<BambuserVideoView />
Props
| Prop | Type | Default | Platform | Description |
|---|---|---|---|---|
| id | string | required | Both | Video identifier. |
| server | 'US' \| 'EU' | 'US' | Both | Organization server region. |
| mode | 'live' \| 'shoppable' | 'live' | Both | Video mode. Use 'shoppable' for on-demand shoppable videos. See Shoppable Video. |
| events | string[] | ['*'] | Both | Custom events to subscribe to. Changing this prop creates a new player. |
| configuration | Record<string, any> | {} | Both | Player configuration. Changing this prop creates a new player — memoize it. |
| ignoredSafeAreaEdges | SafeAreaEdge[] | [] | Both | Edges to ignore when the player computes safe-area padding. mode="live" only; shoppable players never receive safe-area padding on either platform, since they are typically embedded rather than fullscreen. |
| videoScaleMode | 'fit' \| 'fill' | 'fit' | Both | How the video is scaled inside the player view. 'fit' keeps the whole video visible, 'fill' covers the view and crops the overflowing axis. The aspect ratio is always preserved, and the web overlay always fills the view. Changing this prop creates a new player. |
| onEvent | (e) => void | — | Both | Player emitted a custom event. See Event Payloads. |
| onStatus | (e) => void | — | Both | Playback state changed. |
| onProgress | (e) => void | — | Both | Periodic progress updates. |
| onError | (e) => void | — | Both | An error occurred. |
| onPiPStateChanged | (e) => void | — | Both | Picture-in-Picture state changed. |
| onThumbnailTapped | (e) => void | — | iOS only | User tapped the video thumbnail (pre-play). Playback starts automatically on the tap, matching Android's native tap-to-play; the callback is informational. |
⚠️ Memoize
configuration. Passing a new object literal on every render rebuilds the player. UseuseMemo, a module-level constant, or a state-stable object.
Ref Methods
const playerRef = useRef<BambuserVideoViewRef>(null);| Method | Signature | Notes |
|---|---|---|
| play() | () => void | Resume playback. |
| pause() | () => void | Pause playback. |
| mute() | () => void | Mute audio. |
| unMute() | () => void | Unmute audio. |
| seek(seconds) | (number) => void | Seek to seconds. No effect on live broadcasts; shoppable/archived only. |
| setMode(mode) | (BambuserShoppableMode) => Promise<void> | Switch a shoppable player between 'preview' and 'fullExperience'. Resolves when the mode change completes; rejects if it fails or the player is not ready yet. |
| resetPlayer() | () => void | Reset a shoppable player to preview, rewind to 0, re-show thumbnail. iOS only — Android is a no-op (remount to reset). |
| startPiP() | () => void | Enter Picture-in-Picture. |
| stopPiP() | () => void | Exit Picture-in-Picture. |
| setPiPEnabled(enabled) | (boolean) => void | Enable/disable the PiP feature. Defaults to true. |
| invoke(fn, args) | (string, string) => Promise<any> | Call a player-side function. See Player Bridge. |
| notify(callbackKey, info) | (string, NotifyInfo) => void | Reply to a callback event surfaced via onEvent. |
| getCurrentPlayerState() | () => Promise<BambuserPlayerState> | Read the last-known player state. |
| getNativeTag() | () => number \| null | Underlying native view tag. Escape hatch — rarely needed. |
| cleanup() | () => void | Tear down the player and release native resources. Irreversible — see Lifecycle. |
BambuserSDK
For SDK-level operations that don't require a player view: analytics tracking and shoppable collection lookup.
import { BambuserSDK } from '@bambuser/react-native-commerce-sdk';
const sdk = new BambuserSDK({ server: 'US' });
await sdk.track('purchase', {
orderId: 'order-123',
total: 99.99,
currency: 'USD',
});| Method | Signature | Description |
|---|---|---|
| track | (event: string, data: Record<string, any>) => Promise<Record<string, any> \| null> | Send tracking data to Bambuser Analytics. Resolves with the response payload, or null. |
| fetchShoppableCollection | (request: BambuserShoppableCollectionRequest) => Promise<BambuserShoppableCollectionPage> | Fetch a page of shoppable video IDs from a playlist, SKU, or group collection. See Shoppable Video. |
| fetchShoppableCollectionMetadata | (request: BambuserShoppableCollectionMetadataRequest) => Promise<BambuserShoppableCollectionMetadataPage> | Fetch metadata (title, poster URL, duration, audio flag) for a page of shoppable videos. Creates no players; ideal for rendering lightweight thumbnails before mounting any player. |
| clearShoppableCache | () => void | Release prefetched shoppable players not currently mounted. Call when leaving the shoppable screen. No-op on platforms without a native cache. |
Shoppable Video
Shoppable (on-demand) videos are looked up as a collection, then rendered by ID. The flow is:
- Call
sdk.fetchShoppableCollection(...)to get a page ofvideoIds. On iOS this also creates the players behind those ids, so it must run before rendering. - Render each ID with
<BambuserVideoView mode="shoppable" id={...} />. - Optionally switch a player between preview and full-experience with
setMode, or reset it withresetPlayer. - Optionally call
sdk.clearShoppableCache()when leaving shoppable content entirely. On iOS this destroys the players behind the fetched ids, so render them again only after a fresh fetch; the cache is also flushed automatically on the next fetch. No-op on Android.
The ids are mount tokens: put them in state and render each one. The same code works on both platforms; iOS attaches the players the fetch already created, Android builds them on mount.
Need the actual video data, real video ids, titles, durations, poster URLs? That's what fetchShoppableCollectionMetadata is for; it returns stable video ids and display fields without creating any players.
⚠️ Treat
videoIdsas temporary handles, not stable identifiers. The two platforms return different things behind the same field: on Android they are real video ids, on iOS they identify the player instances the fetch just created. In practice:
- Always fetch before rendering, and only render ids from your latest fetch. A new fetch or
clearShoppableCache()invalidates the old ones (on iOS a stale id fails viaonError).- Don't store the ids or send them anywhere; they aren't stable across fetches.
- Don't match them against ids from
fetchShoppableCollectionMetadata; on iOS they won't match. Both calls return the collection in the same order, so pair the lists by position.
For lightweight previews without creating any players (titles, durations, poster URLs for plain <Image> thumbnails), use fetchShoppableCollectionMetadata with the same playlist/SKU/group sources.
A collection comes from a playlist (orgId + componentId), a SKU (orgId + sku), or a group (orgId + groupId):
import { useEffect, useMemo, useRef, useState } from 'react';
import {
BambuserSDK,
BambuserVideoView,
type BambuserVideoViewRef,
} from '@bambuser/react-native-commerce-sdk';
// Memoize — must match the view's `configuration` prop (iOS caches players by config).
// Note: shoppable players never autoplay; playback starts on tap or via play().
const SHOPPABLE_CONFIG = {
thumbnail: { enabled: true },
currency: 'USD',
locale: 'en-US',
};
export function ShoppableScreen() {
const sdk = useMemo(() => new BambuserSDK({ server: 'US' }), []);
const [videoIds, setVideoIds] = useState<string[]>([]);
// One ref per video id; a single shared ref would always point at the
// last-mounted player.
const playerRefs = useRef<Record<string, BambuserVideoViewRef | null>>({});
useEffect(() => {
sdk
.fetchShoppableCollection({
source: 'playlist',
orgId: 'your-org-id',
componentId: 'your-component-id',
page: 1,
pageSize: 15,
configuration: SHOPPABLE_CONFIG,
})
.then((res) => setVideoIds(res.videoIds))
.catch((e) => console.error(e));
// Release prefetched players when leaving the screen. Only clear when
// no shoppable views are mounted; a view whose cached player was cleared
// has nothing to attach to.
return () => sdk.clearShoppableCache();
}, [sdk]);
return (
<>
{videoIds.map((id) => (
<BambuserVideoView
key={id}
ref={(r) => {
playerRefs.current[id] = r;
}}
mode="shoppable"
id={id}
server="US"
style={{ flex: 1 }}
configuration={SHOPPABLE_CONFIG}
/>
))}
</>
);
}SKU- and group-based collections use the same call with a different source:
await sdk.fetchShoppableCollection({
source: 'sku',
orgId: 'your-org-id',
sku: 'your-sku',
configuration: SHOPPABLE_CONFIG,
});
await sdk.fetchShoppableCollection({
source: 'group',
orgId: 'your-org-id',
groupId: 'your-group-id',
configuration: SHOPPABLE_CONFIG,
});Request (BambuserShoppableCollectionRequest):
| Field | Type | Default | Notes |
|---|---|---|---|
| source | 'playlist' \| 'sku' \| 'group' | required | Collection type. |
| orgId | string | required | Organization ID. |
| componentId | string | required (playlist) | Playlist component ID. Playlist source only. |
| sku | string | required (sku) | Product SKU. SKU source only. |
| groupId | string | required (group) | Video group ID. Group source only. |
| page | number | 1 | Page to fetch. |
| pageSize | number | 15 | Items per page. |
| configuration | Record<string, any> | {} | Config for prefetched players. Must match the view's configuration prop (iOS caches players). |
| videoScaleMode | 'fit' \| 'fill' | 'fit' | Scale mode for prefetched players. Must match the view's videoScaleMode prop (iOS caches players). |
Response (BambuserShoppableCollectionPage):
{
videoIds: string[];
pagination: {
page: number | null;
pageSize: number | null;
total: number | null;
totalPages: number | null;
};
}Starting playback
Shoppable players never autoplay. Playback starts when the user taps the thumbnail, or programmatically via play(). Calls to play() before the player reports 'ready' via onStatus are ignored, so trigger an initial play() from your onStatus handler.
Preview vs full experience
A shoppable player starts in preview. Switch it to the full experience (and back) with setMode:
await playerRef.current?.setMode('fullExperience');
await playerRef.current?.setMode('preview');resetPlayer() forces the player back to preview, rewinds to 0, and re-shows the thumbnail. It's iOS only; on Android, remount the view (e.g. change key) to reset.
Event Payloads
All callbacks receive a synthetic event with the payload under e.nativeEvent.
onStatus
{ id: string; state: BambuserPlayerState }state is one of:
'ready' · 'loading' · 'playing' · 'paused' · 'stopped' · 'completed' · 'error' · 'idle' · 'buffering'
onProgress
{ id: string; duration: number; currentTime: number }Both values are in seconds.
onError
{ id: string; message: string }onEvent
{ id: string; type: string; data: any }Custom events emitted by the player (add-to-cart, wishlist, share, etc.). The data shape depends on the event type — see the Bambuser docs for the full event catalog.
Platform difference —
callbackKey: For events that expect a response, the callback key lives atdata.callbackKeyon iOS and at the top level (e.nativeEvent.callbackKey) on Android. Read both:const callbackKey = e.nativeEvent.callbackKey ?? e.nativeEvent.data?.callbackKey;
Platform difference — event data shape: Some event payloads nest data under
data.eventon iOS and at the top level on Android. For example, anadd-to-wishlistSKU is atdata.event.sku(iOS) ordata.sku(Android).
onPiPStateChanged
{ id: string; state: BambuserPiPState }state is one of:
| State | Fires when | Platform |
|---|---|---|
| 'willStart' | Right before PiP begins. | Both |
| 'started' | PiP is active. | Both |
| 'willStop' | Right before PiP ends (programmatic stop). | Both |
| 'stopped' | PiP has ended. | Both |
| 'restored' | User tapped "Go to full screen" from the PiP window. | iOS only |
onThumbnailTapped (iOS)
{ id: string }Player Bridge: invoke & notify
The Bambuser player runs in a webview internally. Two primitives bridge JS ↔ player:
invoke(fn, args)— call a function on the player. Returns a Promise. Use this to drive UI commands (e.g. show/hide overlays).await playerRef.current?.invoke('hideUI', ''); await playerRef.current?.invoke('showUI', '');notify(callbackKey, info)— respond to a callback event the player surfaced viaonEvent. ThecallbackKeycomes from the event payload;infois your response.function onEvent(e) { const { type, data } = e.nativeEvent; const callbackKey = e.nativeEvent.callbackKey ?? data?.callbackKey; if (type === 'add-to-wishlist' && callbackKey) { const sku = Platform.OS === 'ios' ? data?.event?.sku : data?.sku; // ... your wishlist logic playerRef.current?.notify(callbackKey, { success: true, sku }); } }
info accepts booleans, numbers, strings, plain objects/arrays, or null. Strings that already look like JS literals ({...}, [...], numbers, true/false/null) are passed through; anything else is JSON-encoded.
Picture-in-Picture
PiP is enabled by default. Disable per-component with setPiPEnabled(false), or trigger it manually with startPiP() / stopPiP().
playerRef.current?.startPiP();
playerRef.current?.stopPiP();
playerRef.current?.setPiPEnabled(false);Platform behavior
iOS — PiP is view-level. Only the video floats; the rest of your app is unaffected. Provided by the SDK's
pipController.Android — PiP is activity-level. The entire host activity shrinks into the PiP window. You should hide non-video UI when entering PiP and restore it on exit:
const [inPiP, setInPiP] = useState(false); <BambuserVideoView onPiPStateChanged={(e) => setInPiP(e.nativeEvent.state === 'started')} ... /> {!inPiP && <YourAppChrome />}
State Vocabulary
For type-safe state handling:
import type {
BambuserPlayerState,
BambuserPiPState,
} from '@bambuser/react-native-commerce-sdk';
function describe(state: BambuserPlayerState) {
switch (state) {
case 'playing': return 'Now playing';
case 'buffering': return 'Buffering…';
case 'paused': return 'Paused';
// ...
}
}Lifecycle & Cleanup
The player automatically releases native resources when the component unmounts. You normally don't need to call cleanup() yourself.
Call cleanup() explicitly when:
- You're about to navigate away and want to free resources immediately, or
- You want to stop the video before unmount (e.g. the user backed out via a custom gesture).
⚠️
cleanup()is irreversible. Once called, the player is gone. To play again, unmount and remount the component (or changeidto force a rebuild).
useEffect(() => () => playerRef.current?.cleanup(), []);Configuration Reference
configuration is forwarded to the underlying SDK. Common options:
configuration={{
autoplay: true,
currency: 'USD',
locale: 'en-US',
buttons: { dismiss: 'none' },
ui: { hideShareButton: true, hideEmojiOverlay: true },
}}For the complete list of supported keys (per-event, per-feature) consult the Bambuser Live Video docs.
Types
type SafeAreaEdge = 'all' | 'top' | 'bottom' | 'leading' | 'trailing';
type BambuserVideoScaleMode = 'fit' | 'fill';
type BambuserPlayerState =
| 'ready' | 'loading' | 'playing' | 'paused' | 'stopped'
| 'completed' | 'error' | 'idle' | 'buffering';
type BambuserPiPState =
| 'willStart' | 'willStop' | 'started' | 'stopped' | 'restored';
type BambuserSDKOptions = { server?: 'US' | 'EU' };
type BambuserShoppableMode = 'preview' | 'fullExperience';
type BambuserShoppablePlaylistSource = {
source: 'playlist';
orgId: string;
componentId: string;
};
type BambuserShoppableSkuSource = {
source: 'sku';
orgId: string;
sku: string;
};
type BambuserShoppableGroupSource = {
source: 'group';
orgId: string;
groupId: string;
};
type BambuserShoppableCollectionRequest =
(BambuserShoppablePlaylistSource | BambuserShoppableSkuSource | BambuserShoppableGroupSource) & {
page?: number;
pageSize?: number;
configuration?: Record<string, any>;
videoScaleMode?: BambuserVideoScaleMode;
};
type BambuserShoppableCollectionPage = {
videoIds: string[];
pagination: {
page: number | null;
pageSize: number | null;
total: number | null;
totalPages: number | null;
};
};
type BambuserShoppableCollectionMetadataRequest =
(BambuserShoppablePlaylistSource | BambuserShoppableSkuSource | BambuserShoppableGroupSource) & {
page?: number;
pageSize?: number;
};
type BambuserShoppableVideoMetadata = {
id: string;
title: string | null;
hasAudio: boolean | null;
preview: string | null; // poster image URL
length: number | null; // seconds
};
type BambuserShoppableCollectionMetadataPage = {
videos: BambuserShoppableVideoMetadata[];
pagination: BambuserShoppableCollectionPage['pagination'];
};
type NotifyInfo =
| boolean | number | string | null | undefined
| Record<string, any> | Array<any>;Troubleshooting
iOS — pod install fails to find the framework.
The xcframework is vendored inside the package. If pods can't resolve it, delete ios/Pods and ios/Podfile.lock, then run pod install again.
Android — build fails with a Compose-related error.
Make sure the Compose Kotlin plugin is declared in your root build.gradle and that your Kotlin version is compatible (Kotlin >= 2.1 recommended).
Android — minSdkVersion errors.
The SDK requires API 26+. Bump minSdkVersion in your project's android/build.gradle.
Player rebuilds on every render.
You're passing a new configuration (or events) object literal each render. Memoize it with useMemo or hoist it to module scope.
onEvent callbacks fire but callbackKey is undefined.
The location differs across platforms — read both: e.nativeEvent.callbackKey ?? e.nativeEvent.data?.callbackKey.
Documentation
- Bambuser Live Video Documentation — player configuration, event catalog, integration guides.
