npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-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

| 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. Use useMemo, 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:

  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):

| 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 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:

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