@bambuser/react-native-commerce-sdk
v1.0.0-beta.1
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.0.0 and 2.6.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 | ✅ | ✅ |
| Analytics tracking (BambuserSDK.track) | ✅ | ✅ |
v1 scope: live videos only. Shoppable / on-demand video is not included in this release.
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.
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' | 'live' | Both | Video mode. |
| 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. |
| 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). |
⚠️ 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. |
| 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 (currently: analytics tracking).
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. |
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 BambuserPlayerState =
| 'ready' | 'loading' | 'playing' | 'paused' | 'stopped'
| 'completed' | 'error' | 'idle' | 'buffering';
type BambuserPiPState =
| 'willStart' | 'willStop' | 'started' | 'stopped' | 'restored';
type BambuserSDKOptions = { server?: 'US' | 'EU' };
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.
