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

@billdog.io/expo

v1.1.0-beta.3

Published

Expo-compatible paywall rendering library for BillDog

Readme

BillDog Expo Paywalls SDK

Expo-compatible paywall rendering library for BillDog. Display beautiful, high-converting subscription paywalls in your Expo app with smart caching, A/B testing, and analytics built-in.

Features

  • ✅ Expo Compatible: Works seamlessly with Expo managed workflow and EAS Build
  • ✅ Smart Caching: Intelligent prefetching and offline support
  • ✅ A/B Testing: Built-in experiment support
  • ✅ Analytics: Track conversions and user behavior automatically
  • ✅ Variable Substitution: Personalize paywalls with user data
  • ✅ Custom Themes: Match your app's design system
  • ✅ TypeScript: Full type safety
  • ✅ BillDog Auth: Sign-in, sessions, MFA and organizations (development build required)

Installation

For Expo SDK 50+

npx expo install @billdog.io/expo

Add Config Plugin

Add the plugin to your app.json:

{
  "expo": {
    "plugins": [
      "@billdog.io/expo"
    ]
  }
}

Rebuild Your App

After adding the plugin, you need to rebuild your app:

# For EAS Build
eas build --platform all

# For local development builds
npx expo run:ios
npx expo run:android

Quick Start

Configure the SDK once at startup; every component and hook reads the API key from it:

import { BillDogConfiguration } from '@billdog.io/expo';

BillDogConfiguration.configure({ apiKey: 'your_api_key' });
import { PaywallView } from '@billdog.io/expo';

export default function SubscriptionScreen() {
  return (
    <PaywallView
      paywallIdentifier="premium_offer"
      onButtonPress={(buttonId, action) => {
        console.log('Button pressed:', buttonId, action);
        // Handle purchase action
      }}
      onDismiss={() => {
        console.log('Paywall dismissed');
      }}
    />
  );
}

Using with Expo Go

Important: everything that talks to the native BillDog SDK needs a custom development build (npx expo run:ios / npx expo run:android or an EAS build) and does not work in Expo Go. That covers purchases, the headless offering reads (getOfferings, getOffering, useOfferings), deleteAccount, BillDog Auth and the native paywall view. In Expo Go, render a paywall with WebViewPaywall.

import { isExpoGo, WebViewPaywall, NativePaywallView } from '@billdog.io/expo';

const Paywall = isExpoGo() ? WebViewPaywall : NativePaywallView;

Headless (bring your own UI)

The offering reads go through the native SDK, so they carry the install's device proof, the session and the SDK cache. There is one offering model (Offering) and one path.

import {
  BillDog,
  useOfferings,
  PlacementNotFoundError,
  BillDogApiError,
} from '@billdog.io/expo';

function Plans() {
  const { offerings, currentOffering, loading, error } = useOfferings();

  if (loading) return <Text>Loading...</Text>;
  if (error) return <Text>{error.code}: {error.message}</Text>;

  return (
    <View>
      {(currentOffering?.packages ?? []).map((pkg) => (
        <Text key={pkg.identifier}>{pkg.identifier}</Text>
      ))}
    </View>
  );
}

// A placement: the server's decision is on the result.
async function showOnboardingOffer() {
  try {
    const result = await BillDog.getOffering('onboarding');
    if (result.presentation.holdout) return;          // present nothing
    if (!result.offering) return;                     // nothing selected (see presentation.withheld)
    // result.presentation.gated: the feature behind this placement requires access
    // render result.offering.packages with your own UI
  } catch (e) {
    if (e instanceof PlacementNotFoundError) { /* unknown placement identifier */ }
    else if (e instanceof BillDogApiError) { /* e.code: PROOF_REQUIRED, PROOF_INVALID, ... */ }
  }
}

getOffering(placement) never returns the default offering in place of the one asked for.

Advanced Features

Smart Caching

Enable advanced caching for faster load times:

import { PaywallView } from '@billdog.io/expo';

<PaywallView
  paywallIdentifier="premium_offer"
  enableCache={true}
  cacheConfig={{
    maxAge: 3600000, // 1 hour
    staleWhileRevalidate: 86400000, // 24 hours
    prefetchRelated: true,
  }}
/>

Variable Substitution

Personalize paywalls with user data:

<PaywallView
  paywallIdentifier="premium_offer"
  variables={{
    user_name: 'John',
    trial_days: '7',
    discount_percentage: '50',
  }}
/>

A/B Testing

Paywalls automatically participate in experiments:

<PaywallView
  paywallIdentifier="premium_offer"
  userContext={{
    user_id: 'user_123',
    segment: 'power_users',
  }}
/>

Custom Loading & Error States

<PaywallView
  paywallIdentifier="premium_offer"
  LoadingComponent={() => (
    <View style={styles.loading}>
      <ActivityIndicator size="large" />
      <Text>Loading your offer...</Text>
    </View>
  )}
  ErrorComponent={({ error, retry }) => (
    <View style={styles.error}>
      <Text>Oops! {error.message}</Text>
      <Button title="Retry" onPress={retry} />
    </View>
  )}
/>

BillDog Auth

Re-exported straight from @billdog.io/react-native, which owns the native bridge — so the refresh token lives in the iOS Keychain and Android EncryptedSharedPreferences, not in AsyncStorage.

Not available in Expo Go. The native module is not part of the Expo Go binary. Use a development build (npx expo run:ios / eas build --profile development). BillDogAuth.isAvailable reports this up front rather than failing at the first sign-in attempt. Requires native binaries 1.1.0-beta.3 — auth is absent from the published beta.2 iOS XCFramework and has no Android artifact before that at all.

import { BillDogAuth } from '@billdog.io/expo';

if (!BillDogAuth.isAvailable) {
  // Running in Expo Go — send the user to hosted sign-in, or prompt for a
  // development build.
  return;
}

await BillDogAuth.restore();   // { signedIn: false } on first launch

const r = await BillDogAuth.attempt({
  identifier: '[email protected]',
  strategy: 'password',
  password,
});
if (r.mfaRequired) {
  await BillDogAuth.attempt({
    identifier: '[email protected]',
    strategy: 'totp',
    code,
    mfaToken: r.mfaToken,
  });
}

Phone sign-in needs kind: 'phone' on both requestCode and attempt — it selects the identity row, and omitting it looks a phone number up among email addresses and matches nothing.

The full surface, its organization methods and its limitations — no captcha or invitation token on sign-up, no password reset, no self-serve account deletion — are documented in the React Native README.

Hooks API

usePaywall

Fetch a single paywall with caching:

import { usePaywall } from '@billdog.io/expo';

function MyComponent() {
  const { configuration, loading, error, refetch } = usePaywall({
    paywallIdentifier: 'premium_offer',
    enableCache: true,
  });

  if (loading) return <Text>Loading...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;

  return <Text>Paywall: {configuration?.name}</Text>;
}

useOfferings

The project's default offering read (no placement). Returns the public Offering model.

import { useOfferings } from '@billdog.io/expo';

function OfferingsScreen() {
  const { offerings, currentOffering, loading, error, refetch, getOffering } = useOfferings();

  return (
    <FlatList
      data={offerings}
      keyExtractor={(offering) => offering.id}
      renderItem={({ item }) => <OfferingCard offering={item} selected={item.id === currentOffering?.id} />}
      onRefresh={refetch}
      refreshing={loading}
    />
  );
}

currentOffering is the offering the server selected for this caller, or null when it selected none. Pass { skipFetch: true } to read later with refetch().

Utilities

Expo Go Detection

import { isExpoGo, isStandaloneBuild } from '@billdog.io/expo';

if (isExpoGo()) {
  console.log('Running in Expo Go - native features are unavailable; use WebViewPaywall');
} else if (isStandaloneBuild()) {
  console.log('Running in standalone build - full native support');
}

Configuration Options

| Prop | Type | Required | Description | |------|------|----------|-------------| | paywallIdentifier | string | Yes | Unique identifier for the paywall | | configuration | PaywallConfiguration | No | Direct configuration (bypasses API) | | onButtonPress | (id: string, action: any) => void | No | Button press handler | | onDismiss | () => void | No | Dismiss handler | | variables | Record<string, string> | No | Variable substitution values | | userContext | Record<string, any> | No | User context for A/B testing | | enableCache | boolean | No | Enable smart caching (default: false) | | cacheConfig | CacheConfig | No | Advanced cache configuration | | LoadingComponent | React.ComponentType | No | Custom loading component | | ErrorComponent | React.ComponentType | No | Custom error component |

TypeScript Support

Full TypeScript definitions included:

import type {
  PaywallConfiguration,
  PaywallComponent,
  OfferingPackage,
  UsePaywallOptions,
  UseOfferingsOptions,
} from '@billdog.io/expo';

Troubleshooting

"Native module not found" Error

Make sure you've:

  1. Added the plugin to app.json
  2. Rebuilt your app with eas build or npx expo run:ios/android
  3. Installed all dependencies with npx expo install

Paywall Not Displaying in Expo Go

Native paywall rendering requires a custom build. In Expo Go, render with WebViewPaywall. The headless reads (useOfferings, BillDog.getOffering) also need a custom build.

Cache Not Working

Ensure you've enabled caching:

<PaywallView enableCache={true} />

Analytics Events Not Firing

Check that your API key has analytics permissions and that you're passing userContext for user tracking.

Migration from React Native SDK

If you're migrating from @billdog.io/react-native, simply:

  1. Replace imports:
// Before
import { PaywallView } from '@billdog.io/react-native';

// After
import { PaywallView } from '@billdog.io/expo';
  1. Add the config plugin to app.json
  2. Rebuild your app

All APIs are identical!

Next Steps

Support

  • Documentation: https://docs.billdog.app
  • GitHub Issues: https://github.com/billdog/expo-paywalls/issues
  • Discord: https://discord.gg/billdog

License

MIT © BillDog