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

@tiledev/sdk-apptile-live-selling

v0.2.0

Published

Live selling, auctions and giveaways for React Native — the Apptile live-selling gateway as a typed singleton, a Zego room engine behind a platform boundary, an assembled session state machine, and system picture-in-picture with its own config plugin. Com

Readme

@tiledev/sdk-apptile-live-selling

Live selling, auctions and giveaways for React Native — the Apptile live-selling gateway as a typed singleton, a Zego room engine, an assembled session state machine, and system picture-in-picture with its own config plugin.

Headless. It ships one component (LiveVideoTarget, because a render target is unavoidably native). Your header, comments list, bid bar and sheets stay yours.

Storefront-agnostic. Products, cart writes and the auction checkout come from an adapter you supply, so there is no Shopify dependency.

npm i @tiledev/sdk-apptile-live-selling

Peers: react and react-native are needed by the main entry, which re-exports the React layer; expo and zego-express-engine-reactnative stay optional — off-device the engine and PIP resolve to no-ops.

Everything ships from the package root, including the providers, the hooks and LiveVideoTarget. There is no ./react subpath — a subpath is reachable only through the exports map, which a resolver that ignores it (node10, some test runners and bundlers) cannot see at all. What remains beside the root is ./types (types alone) and a wildcard for any module in dist/session, /streams, /polls — the escape hatch below.

The root entry pulls in react-native, so it no longer loads in a plain Node runtime. Driving the pure data layer from Node — sessionReducer, the wire mappers — goes through the module subpaths (@tiledev/sdk-apptile-live-selling/session), which stay free of React and react-native.

1. Add the config plugin

Every native entry live selling needs lives in a file expo prebuild regenerates, so it has to be a plugin — an edit by hand works on the machine that made it and silently stops working in CI.

{
  "expo": {
    "plugins": [
      ["@tiledev/sdk-apptile-live-selling", {
        "cameraPermission": "Used to appear on camera when you co-host a live show."
      }]
    ]
  }
}

| Prop | Default | What it writes | |---|---|---| | pip | true | android:supportsPictureInPicture, the configChanges a PIP resize needs, UIBackgroundModes: ["audio"] | | cameraPermission / microphonePermission | generic copy | NSCameraUsageDescription / NSMicrophoneUsageDescription — never overwrites strings your app already sets | | capturePermissions | true | Android CAMERA + RECORD_AUDIO |

Zego's own Maven repo is added unconditionally: im.zego:express-video lives only there, and a clean Android checkout fails at configuration time without it.

Then npx expo prebuild and run the app. The LivePip Expo module autolinks itself.

If your app already vendors a live-pip module, delete it — identical module and pod names collide at link time.

2. Configure

Every viewer endpoint is anonymous: the brand companyId is the only credential, sent as x-company-id. It is not the Shopify store id and not the Tile app id.

import { liveSelling } from '@tiledev/sdk-apptile-live-selling';

liveSelling.configure({
  apiUrl: 'https://live-selling.apptile.io',
  companyId: '<brand-id>',
  zegoAppId: 361108744,   // platform-level: the gateway signs room tokens with one Zego app
  commerce,               // see §4
  identity,
});

| Field | Required | Purpose | |---|---|---| | apiUrl | yes | Gateway base URL, no trailing slash | | companyId | yes | Apptile "brand id" — the only credential | | zegoAppId | for video | Matches the gateway, not the brand. Native engine only | | commerce | for shopping | Resolves lots to products and sells them | | identity | for bidding | The customer a win is attributed to | | storage | no | AsyncStorage-shaped. Defaults to localStorage, else in-memory | | videoCollectionId | no | The clip reel. Not scoped by the company header, so a stale id serves another tenant's clips — unset ⇒ clips.list() is [] | | requestSource | no | X-Request-Source. Defaults to "APPTILE" | | logger | no | { error }. Defaults to silent | | customerAccountRequest | no | The customer's bearer transport. Only the unpaid-wins read uses it |

Pass real storage in anything you ship. Three things are lost on relaunch without it: the name a signed-out shopper gave, the accepted auction terms, and which polls this install voted in. The last one does damage — the gateway does not dedupe votes, so a forgotten vote lets the same device vote again.

3. Mount and render

LiveSellingProvider configures the client. LiveSessionProvider folds the engine's events and the polled record into one renderable state — mount it above your navigator, because the Zego engine holds exactly one room and two places usually render the same stream (a home card and the full-screen player). Own it per screen and whichever unmounts first kills the other's video.

import {
  LiveSellingProvider, LiveSessionProvider, useLiveSession, LiveVideoTarget,
} from '@tiledev/sdk-apptile-live-selling';

<LiveSellingProvider config={config}>
  <LiveSessionProvider onPipRestore={() => navigate('Live')}>
    <Navigator />
  </LiveSessionProvider>
</LiveSellingProvider>

Surfaces don't own the video, they bind to it:

function LiveScreen() {
  const session = useLiveSession();
  const isFocused = useIsFocused();

  return (
    <LiveVideoTarget active={isFocused && session.pipMode === 'none'}>
      {session.status !== 'playing' && <YourPoster status={session.status} />}
    </LiveVideoTarget>
  );
}

active is a prop rather than read from navigation focus, so the package needs no navigator dependency — and a PIP surface drawn outside the navigator has no route to read focus from anyway. Exactly one mounted target should have it set.

What useLiveSession() gives you

| | | |---|---| | status | loading · connecting · waitingForHost · playing · ended · error | | stream | the record: title, host, products, giveaways, bidCap, reactionStyle | | activeProduct products | resolved through your commerce adapter | | comments reactions sold poll | the live feed; reactions and sold cards expire on their own | | auction | biddingOpen, highestBid, nextBid, timeLeftMs, isLeading, leaderName, extended, lastWin | | giveaway | active, entryCount, registered, result | | guest | co-host: mode, status, slot, mutedByHost, mixerLayout | | viewerCount elapsedMs muted pipMode viewerName | header material | | actions | sendComment, sendReaction, addToCart, voteInPoll, placeBid, enterGiveaway, enterPip, joinAsGuest, … |

actions is identity-stable — safe in a dependency array, and deliberately so: one of the effects that depends on it rebinds the video, which is a real startPlayingStream.

viewerName is null when nobody has said who they are. Ask before the first comment goes out rather than publishing "Anonymous" to a room; actions.chooseViewerName(raw) remembers the answer.

capabilitiesFor(stream) returns the flag set a stream calls for (canBid, canEnterGiveaway, showLiveBadge, …), so one screen serves an ordinary live show and an auction. An auction is a live show plus bidding — the flags are derived, not spelled out twice.

4. Adapters

The gateway only ever speaks in bare numeric product ids, because that is what the host dashboard records. Turning one into something with a price, an image and variants is your job — and so is selling.

const commerce = {
  productsByIds: async (ids) => {
    const products = await shopify.products.byIds(ids.map((id) => `gid://shopify/Product/${id}`));
    return products.map((p) => ({
      id: p.id,
      storeProductId: p.id.split('/').pop(),
      title: p.title,
      price: p.priceRange.min.amount,
      currencyCode: p.priceRange.min.currencyCode,
      imageUrl: p.images?.[0]?.url ?? null,
      variants: p.variants.map((v) => ({
        id: v.id, title: v.title, price: v.price.amount, available: v.availableForSale,
      })),
      raw: p,                       // carried through untouched, for your own call sites
    }));
  },
  // FALSE for a *refused* line rather than throwing. A refused add must not be announced to the
  // room — that card is what everyone else reads as "it is going".
  addLine: (variantId, quantity) => shopify.cart.addLine({ merchandiseId: variantId, quantity }),
};

const identity = {
  bidderId: () => customerId,                  // bare id; what the host echoes back on a bid
  customerRef: () => `gid://shopify/Customer/${customerId}`,
  customerName: () => customer.firstName,
};

Only productsByIds is required. An unimplemented method disables the feature that needs it rather than throwing: no addLine ⇒ no add-to-cart, no createAuctionCheckout ⇒ wins are reported but not payable.

raw keeps its type. Name it once and your own product comes back off the session with no cast:

const session = useLiveSession<ShopifyProduct>();
session.activeProduct?.raw?.onlineStoreUrl;   // ShopifyProduct | undefined, not unknown

bidderId must be the same id the host's broadcast echoes back, or isLeading compares two different things and nobody is ever leading. Keep it distinct from the per-install viewer id, which is what the host mutes by.

Build a disposable cart, not the shopping cart: an auction win is already a commitment at a settled price, and mixing those lines into a browsing cart lets a quantity stepper change it.

On Shopify each line carries a hidden _winningBid (or _giveawayItem) attribute that a cart transform function on the store reads to reprice the line server-side. Each line also needs something unique in its attributes — Shopify merges lines sharing the same merchandise and attributes, so two identical wins collapse into one quantity-2 line priced as a single lot, and the second win is free.

5. Auctions — the two rules that matter

Nothing is optimistic. A viewer offers a bid to the round's moderator and waits; only the host's broadcast sets the high bid, the leader and the deadline. That is what makes two phones agree.

Deadlines are on the host's clock, carried as serverNowMs on every event that moves them. A phone minutes out of sync would otherwise close a round that is still open. auction.timeLeftMs is already corrected for the offset.

useAuctionBidGate owns the four ordered conditions in front of a bid and reports which one is current; you render the panels. The order is the whole point:

const gate = useAuctionBidGate({
  streamingId: session.stream?.streamingId ?? null,
  isLoggedIn, biddingOpen: auction.biddingOpen,
  variantCount: biddingProduct?.variants.length ?? 0,
  placeBid: actions.placeBid,
});

// gate.blocker: 'signIn' | 'unpaidWins' | 'terms' | 'variant' | null
gate.requestBid();        // the round's minimum — the slide gesture
gate.requestBid(250);     // a custom bid

Signed in → nothing outstanding on another stream → rules accepted once per install → a variant chosen (once per round, not per bid). The requested bid is held while you clear the blockers and then placed, so finishing the last panel completes the bid they originally asked for. Call gate.refreshUnpaidWins() from your own focus effect, so a customer who leaves to settle a tab is unblocked on return.

If you render the panels as React Native Modals, honour SHEET_HANDOFF_MS between them — presenting one while another dismisses is a native iOS exception, not a catchable error.

6. Platform boundaries

Two pairs of files, and Metro picks by platform. TypeScript resolves the non-native one, which is why tsc needs neither Zego nor Expo installed.

| Native (iOS/Android) | Everywhere else | |---|---| | engine.native.ts — the real Zego engine; owns the room, plays into a rebindable view, publishes a guest's camera, translates every Zego channel into a LiveEvent | engine.ts — no-op, isNoop: true | | pip.native.ts — the LivePip Expo module | pip.ts — no-op, isLivePipSupported: false |

So the session never arms a handover that cannot happen, and a web bundle never evaluates the Zego entry module.

Picture-in-picture is two stages. actions.enterPip() shrinks to a surface you draw; the session arms the OS handover in advance, so minimising the app opens a real system window with no JavaScript involved at that moment. Coming back raises onPipRestore — navigate to your live screen, which the SDK can't do itself from above the navigator.

7. Imperative client

For anything the session doesn't cover. liveSelling.configure() first; isReady() and companyId() report state.

| Namespace | Methods | |---|---| | streams | list ongoing byId zegoToken | | auctions | recordBid enterGiveaway hasAcceptedTerms acceptTerms unpaidStreamIds wins isBiddingBlocked | | polls | vote loadVotedOption rememberVote forgetVote | | replays | list segments comments incrementViews | | clips | list | | guest | acceptInvite leaveSlot userId | | flags | fetch — per-company feature switches | | viewer | id loadName cachedName saveName normaliseName | | engine | the Zego engine (above) |

Read hooks for screens that only need data: useOngoingLive, useLiveStreams, useStreamInfo, useReplays, useClips, useAuction.

LiveEvent is the union the engine emits — comment, reaction, sold, viewers, productChange, poll*, biddingStarted/bidInfo/biddingClosed/auctionWon, giveaway*, the guest* co-host signals, and an unknown fallthrough so a new host-side message is visible rather than dropped. Subscribe via LiveSellingProvider's onEvent or liveSelling.engine.subscribe.

Scripts

| Command | What it does | |---|---| | npm run build | Cleans, compiles srcdist and plugin/srcplugin/build. Both are needed: app.plugin.js loads the compiled plugin so consumers never need TypeScript at prebuild | | npm run lint | Typechecks both projects, emitting nothing | | npm run clean | Removes dist and plugin/build | | npm pack | The publish tarball — use it to verify dist, ios/, android/, expo-module.config.json, app.plugin.js and plugin/build all ship |

The reducer is exported on its own (sessionReducer, deriveStatus, timeLeftMs) and is pure, so it can be driven with no room, socket or React — from Node, import it as @tiledev/sdk-apptile-live-selling/session rather than off the root.

Gotchas

  • A camera left off is invisible to Zego. No media event and no camera-state event fires; only zero-FPS quality samples reveal it, which is why status becomes waitingForHost about 5s in rather than instantly.
  • A dropped socket is not the end of a stream. Backgrounding disconnects the room and Zego recovers by itself, so only the host's explicit signal or the record ends it.
  • Broadcasts are never replayed. A shadow-mute is seeded from the stream record for exactly this reason — otherwise a muted viewer gets a free message per relaunch, and an unmute is invisible until the app restarts.
  • A guest and a viewer cannot both hold the room. One Zego session per engine, and publishing rights live in the token, so co-hosting logs the viewer out and leaveGuest takes the room back.
  • Consuming via file: or a workspace link? Hoist @types/react. The .d.ts otherwise resolves against the SDK's own nested copy, and React 18's ReactNode isn't assignable to React 19's.

Not included

UI · a storefront · navigation · a static clip fallback (clips.list() reads only the configured collection).

Verified against

React 19.1.0 · React Native 0.81.5 · Expo 54 — typechecked from the packed tarball, plus a Node load of the pure data layer. The native path (autolinking, the podspec, the plugin's prebuild output) has not been exercised on a device yet.