@bambuser/react-native-commerce-sdk

中文文档
2.1.0发布于 2026/09/16TypeScriptCommonJS零依赖近 30 天下载 5658

React Native wrapper for Bambuser Commerce SDK

原始使用文档
文档目录28 节
  1. Feature Overview
  2. Requirements
  3. Installation
  4. iOS
  5. Android
  6. Quick Start
  7. Component API
  8. <BambuserVideoView />
  9. BambuserSDK
  10. Shoppable Video
  11. Starting playback
  12. Preview vs full experience
  13. Event Payloads
  14. onStatus
  15. onProgress
  16. onError
  17. onEvent
  18. onPiPStateChanged
  19. onThumbnailTapped (iOS)
  20. Player Bridge: invoke & notify
  21. Picture-in-Picture
  22. Platform behavior
  23. State Vocabulary
  24. Lifecycle & Cleanup
  25. Configuration Reference
  26. Types
  27. Troubleshooting
  28. Documentation

@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

FeatureiOSAndroid
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-sdk

iOS

cd ios && pod install

The 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

PropTypeDefaultPlatformDescription
idstringrequiredBothVideo identifier.
server'US' | 'EU''US'BothOrganization server region.
mode'live' | 'shoppable''live'BothVideo mode. Use 'shoppable' for on-demand shoppable videos. See Shoppable Video.
eventsstring[]['*']BothCustom events to subscribe to. Changing this prop creates a new player.
configurationRecord<string, any>{}BothPlayer configuration. Changing this prop creates a new player — memoize it.
ignoredSafeAreaEdgesSafeAreaEdge[][]BothEdges 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'BothHow 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—BothPlayer emitted a custom event. See Event Payloads.
onStatus(e) => void—BothPlayback state changed.
onProgress(e) => void—BothPeriodic progress updates.
onError(e) => void—BothAn error occurred.
onPiPStateChanged(e) => void—BothPicture-in-Picture state changed.
onThumbnailTapped(e) => void—iOS onlyUser 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. Use useMemo, a module-level constant, or a state-stable object.

Ref Methods

const playerRef = useRef<BambuserVideoViewRef>(null);
MethodSignatureNotes
play()() => voidResume playback.
pause()() => voidPause playback.
mute()() => voidMute audio.
unMute()() => voidUnmute audio.
seek(seconds)(number) => voidSeek 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()() => voidReset a shoppable player to preview, rewind to 0, re-show thumbnail. iOS only — Android is a no-op (remount to reset).
startPiP()() => voidEnter Picture-in-Picture.
stopPiP()() => voidExit Picture-in-Picture.
setPiPEnabled(enabled)(boolean) => voidEnable/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) => voidReply to a callback event surfaced via onEvent.
getCurrentPlayerState()() => Promise<BambuserPlayerState>Read the last-known player state.
getNativeTag()() => number | nullUnderlying native view tag. Escape hatch — rarely needed.
cleanup()() => voidTear 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',
});
MethodSignatureDescription
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() => voidRelease 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:

  1. Call sdk.fetchShoppableCollection(...) to get a page of videoIds. On iOS this also creates the players behind those ids, so it must run before rendering.
  2. Render each ID with <BambuserVideoView mode="shoppable" id={...} />.
  3. Optionally switch a player between preview and full-experience with setMode, or reset it with resetPlayer.
  4. 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 videoIds as 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 via onError).
  • 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):

FieldTypeDefaultNotes
source'playlist' | 'sku' | 'group'requiredCollection type.
orgIdstringrequiredOrganization ID.
componentIdstringrequired (playlist)Playlist component ID. Playlist source only.
skustringrequired (sku)Product SKU. SKU source only.
groupIdstringrequired (group)Video group ID. Group source only.
pagenumber1Page to fetch.
pageSizenumber15Items per page.
configurationRecord<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 at data.callbackKey on 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.event on iOS and at the top level on Android. For example, an add-to-wishlist SKU is at data.event.sku (iOS) or data.sku (Android).

onPiPStateChanged

{ id: string; state: BambuserPiPState }

state is one of:

StateFires whenPlatform
'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 via onEvent. The callbackKey comes from the event payload; info is 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 change id to 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/react-native-commerce-sdk 共发布 16 个版本,首次发布于 2026/05/18;过去 12 个月发布了 16 个版本。

版本发布日期距今
2.1.0latest2026/09/161 个月前
2.0.02026/08/312 个月前
2.0.0-beta.1beta2026/08/282 个月前
2.0.0-alpha.5alpha2026/08/262 个月前
2.0.0-alpha.42026/08/212 个月前
2.0.0-alpha.32026/08/202 个月前

关于 @bambuser/react-native-commerce-sdk 的常见问题

@bambuser/react-native-commerce-sdk 是什么?

React Native wrapper for Bambuser Commerce SDK

如何安装 @bambuser/react-native-commerce-sdk?

在项目目录运行 npm install @bambuser/react-native-commerce-sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18.0.0)、react-native(>=0.72.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@bambuser/react-native-commerce-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bambuser/react-native-commerce-sdk 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@bambuser/react-native-commerce-sdk 可以商用吗?

@bambuser/react-native-commerce-sdk 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@bambuser/react-native-commerce-sdk 的下载量有多少?

根据 npm 官方下载统计,@bambuser/react-native-commerce-sdk 在 2026/10/08 – 2026/11/06 这 30 天内被下载 5,658 次,日均约 189 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bambuser/react-native-commerce-sdk 还在维护吗?

最新版本 2.1.0 发布于 2026/09/16(1 个月前);至今共发布 16 个版本,其中过去 12 个月发布了 16 个;首次发布于 2026/05/18。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。