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/react-native

v1.1.0-beta.3

Published

React Native paywall rendering library for BillDog

Readme

@billdog.io/react-native

React Native library for rendering BillDog paywalls with native components.

What's New in 1.4 — Purchase Parity APIs

The RN bridge now exposes the full purchase parity surface (PurchaseParams, PurchaseController, CustomProduct, attribution setters, setProxyURL, eligibility, canMakePayments, presentCodeRedemptionSheet, storefront observer):

import { BillDog, PurchaseParams } from '@billdog.io/react-native';

const result = await BillDog.purchase(
  PurchaseParams.builder('pro_yearly')
    .promoOfferId('intro_2024')
    .appAccountToken(uuid)
    .isPersonalizedOffer(true)
    .build()
);
// result.kind: 'purchased' | 'cancelled' | 'pending' | 'failed' | 'restored'

await BillDog.setAdjustID(adid);
await BillDog.setAppsflyerID(appsflyerId);
await BillDog.setProxyURL('https://billdog-edge.example.com');

Full guides: Custom Purchases · Attribution · Self-Hosted Proxy

Your own purchase code (host PurchaseController)

If the app buys through its own code (react-native-purchases, react-native-iap, …), register a PurchaseController at configure. Every purchase — a native paywall button or BillDog.purchase — then runs it through the native SDK, which records the purchase from the store evidence it returns. BillDog never finishes or acknowledges a transaction in this mode.

import { BillDog, BillDogConfiguration, StoreEvidence } from '@billdog.io/react-native';

BillDogConfiguration.configure({
  apiKey: 'bd_...',
  purchaseController: {
    capabilities: [],               // 'quantity' | 'promotional_offers' | 'win_back_offers' | 'upgrades'
    storeKitVersion: 'storeKit2',
    async purchase(productId, request) {
      const tx = await myStore.buy(productId, { accountToken: request?.storeAccountToken });
      return {
        result: { type: 'purchased', transactionId: tx.id, productId },
        evidence: [
          Platform.OS === 'ios'
            ? StoreEvidence.appStore({ transactionId: tx.id, productId })
            : StoreEvidence.googlePlay({ purchaseToken: tx.purchaseToken, productIds: [productId], orderId: tx.orderId }),
        ],
      };
    },
    async restorePurchases() {
      return { type: 'restored', items: [] };
    },
  },
});
  • Return a bare PurchaseResult if you have no evidence: BillDog derives it from the store (on Android by the result's transactionId read as the Play order id), or records the purchase at its next sweep.
  • A purchase your controller does not answer within five minutes, or one in flight when the JavaScript context reloads, is reported pending, never failed: BillDog records it when the store reports it.
  • BillDog.setPurchaseController is gone; pass the controller to configure.
  • With RevenueCat, use @billdog.io/integration-revenuecat.

Purchases your code makes outside a paywall:

const recording = await BillDog.recordPurchase(StoreEvidence.appStore({ transactionId, productId }));
BillDog.observePurchaseResult(productId, { transactionId, evidence });   // analytics + recording
const held = await BillDog.syncStorePurchases();                         // backstop sweep, never prompts
const issues = await BillDog.purchaseRecordingIssues();                  // never shown as success
const token = await BillDog.storeAccountToken();                         // appAccountToken / obfuscatedAccountId

Features

✨ AI-Generated Paywalls - Create paywalls using AI prompts or templates
⚡ Smart Caching - Instant display with <100ms load time (Phase 3)
🎨 Cross-Platform - Works on iOS and Android
📊 Real-time Analytics - Track conversions and revenue
🔌 Offline Support - Cached paywalls work without network
🎯 A/B Testing - Run experiments without app updates
📱 Dynamic Updates - Update paywalls remotely

Advanced Caching System

BillDog SDK includes a production-grade caching system that dramatically improves performance:

⚡ 60% Storage Reduction - GZIP compression automatically reduces cache size
🚀 Sub-50ms Load Times - Multi-level caching (memory + AsyncStorage)
🧠 Intelligent Prefetching - Learns user patterns and preloads paywalls
📊 Real-time Analytics - Monitor cache performance and hit rates
🔄 Background Updates - Silent version checking keeps content fresh
📴 Offline Support - Cached paywalls work without network

Quick Start with Caching

import { PaywallView } from '@billdog.io/react-native';

function MyPaywall() {
  return (
    <PaywallView
      paywallIdentifier="premium_offer"
      cacheConfig={{
        enabled: true,                    // Enable caching (default: true)
        compressionEnabled: true,         // Enable GZIP (default: true)
        memoryCacheEnabled: true,         // Memory cache (default: true)
        maxAge: 24 * 60 * 60 * 1000,     // 24 hours (default: 7 days)
        prefetchEnabled: true,            // Smart prefetching (default: false)
      }}
      onButtonPress={(id, action) => handlePurchase(id, action)}
    />
  );
}

Cache Analytics

import { AdvancedPaywallCache } from '@billdog.io/react-native';

const cache = new AdvancedPaywallCache({
  enabled: true,
  compressionEnabled: true,
});

// Get real-time analytics
const analytics = await cache.getAnalytics();
console.log(`Hit Rate: ${(analytics.hitRate * 100).toFixed(1)}%`);
console.log(`Avg Load Time: ${analytics.avgLoadTime.toFixed(0)}ms`);
console.log(`Storage Saved: ${(analytics.compressionRatio * 100).toFixed(0)}%`);

📖 Full Caching Guide: See CACHING_GUIDE.md for advanced configuration, platform comparison, and best practices.

Installation

npm install @billdog.io/react-native
# or
yarn add @billdog.io/react-native

Usage

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

import { BillDogConfiguration } from '@billdog.io/react-native';

BillDogConfiguration.configure({ apiKey: 'your_api_key' });

Basic Usage

import { PaywallView } from '@billdog.io/react-native';

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

With Direct Configuration

import { PaywallView, PaywallConfiguration } from '@billdog.io/react-native';

const config: PaywallConfiguration = {
  components: [
    {
      id: 'title',
      type: 'text',
      name: 'Title',
      position: { x: 0, y: 0 },
      size: { width: '100%', height: 'auto' },
      layout: { width: 'fill', height: 'hug' },
      appearance: {},
      properties: {
        text: 'Unlock Premium Features',
        fontSize: 24,
        fontWeight: 'bold',
        textAlign: 'center',
      },
    },
    // ... more components
  ],
};

function App() {
  return (
    <PaywallView
      configuration={config}
      onButtonPress={(id, action) => {
        // Handle purchase
      }}
    />
  );
}

Custom Loading and Error Components

import { PaywallView } from '@billdog.io/react-native';
import { Text, View } from 'react-native';

function LoadingPaywall() {
  return (
    <View>
      <Text>Loading your offers...</Text>
    </View>
  );
}

function ErrorPaywall({ error }: { error: Error }) {
  return (
    <View>
      <Text>Failed to load: {error.message}</Text>
    </View>
  );
}

function App() {
  return (
    <PaywallView
      paywallIdentifier="premium_offer"
      LoadingComponent={LoadingPaywall}
      ErrorComponent={ErrorPaywall}
    />
  );
}

API Reference

PaywallView Props

| Prop | Type | Required | Description | |------|------|----------|-------------| | paywallIdentifier | string | No | Paywall identifier to fetch from API | | configuration | PaywallConfiguration | No | Direct configuration (skips API fetch) | | onButtonPress | (id: string, action: string) => void | No | Called when user presses buttons | | onDismiss | () => void | No | Called when paywall is dismissed | | LoadingComponent | ComponentType | No | Custom loading component | | ErrorComponent | ComponentType<{ error: Error }> | No | Custom error component |

usePaywall Hook

import { usePaywall } from '@billdog.io/react-native';

const { configuration, loading, error, refetch } = usePaywall({
  paywallIdentifier: 'premium_offer',
  userContext: {
    app_user_id: 'user_123',
    country: 'US',
    platform: 'ios',
  },
});

Headless offerings (bring your own UI)

One model, one path. The native SDK reads the offering (so the request carries the device proof and the session, and is cached natively) and merges live App Store / Play prices into the products. These calls need the native module: a development or release build, not a JS-only run.

import {
  BillDog,
  PlacementAccessWithheldError,
  PlacementNotFoundError,
  BillDogApiError,
} from '@billdog.io/react-native';

// The project default. No placement is named.
const offerings = await BillDog.getOfferings();           // Offering[]
const current = await BillDog.getCurrentOffering();       // Offering | null

// A placement. The server's decision is surfaced, never replaced.
try {
  const result = await BillDog.getOffering('onboarding'); // OfferingResult
  if (result.presentation.holdout || !result.offering) {
    // Held out, or nothing selected: show nothing.
    return;
  }
  BillDog.trackPaywallPresented(result.offering, result.placement);
  const pkg = result.offering.packages[0];
  await BillDog.purchasePackage(pkg);
} catch (e) {
  if (e instanceof PlacementAccessWithheldError) {
    // Withheld from this caller (e.reason). Keep the feature shut.
  } else if (e instanceof PlacementNotFoundError) {
    // No such placement in this project.
  } else if (e instanceof BillDogApiError) {
    // e.code: PROOF_REQUIRED, PROOF_INVALID, REAUTH_REQUIRED, ACCESS_DENIED
    // (e.reason === 'target_mismatch'), PURCHASE_OWNER_CONFLICT, ...
  }
}

OfferingResult is { offering, offerings, placement, presentation, experiment, campaign }. offering is the offering the server selected for this caller; it is null on a holdout or when nothing is selected, and the SDK never substitutes the current or default offering for it.

useOfferings Hook

The same read as a hook. It returns the same Offering model.

import { useOfferings } from '@billdog.io/react-native';

const { offerings, currentOffering, loading, error, refetch, getOffering } = useOfferings();
  • skipFetch (option): do not read on mount; call refetch() when ready.
  • error is a BillDogApiError (see above), or null.
  • getOffering(placement) is BillDog.getOffering(placement).

BillDog Auth

Sign-in, sessions and second factors. The bridge delegates to the native SDKs rather than reimplementing them, so the refresh token lives in the iOS Keychain and Android EncryptedSharedPreferences — never in AsyncStorage, which is plain text on disk.

Requires 1.1.0-beta.3 of the native binaries. It is not that beta.2 is missing two methods — the whole auth module postdates it. BillDogFull 1.0.0-beta.2 shipped 2026-08-08 and contains no BillDogAuth; Maven Central carries no io.billdog:billdog-auth at any version before beta.3. A build that resolves beta.2 fails to compile.

import { BillDogAuth } from '@billdog.io/react-native';

// False when the native module is not linked into the host app.
if (!BillDogAuth.isAvailable) return;

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

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

// Check this BEFORE treating it as a sign-in: against an enrolled account a
// correct password issues no session.
if (r.mfaRequired) {
  await BillDogAuth.attempt({
    identifier: '[email protected]',
    strategy: 'totp',
    code,
    mfaToken: r.mfaToken,   // proves the first factor was accepted
  });
}

Passwordless, and phone numbers

// A RETURNING user gets a fresh code from requestCode. Without it a code could
// only be obtained as a side effect of signing up.
await BillDogAuth.requestCode({ identifier: '+15551234567', kind: 'phone' });

await BillDogAuth.attempt({
  identifier: '+15551234567',
  kind: 'phone',          // REQUIRED — selects the identity row
  strategy: 'sms_code',
  code,
});

kind defaults to 'email'. A phone sign-in that omits it looks up a phone number among email addresses and silently matches nothing.

The rest of the surface

isSignedIn() · signUp({ identifier, kind?, password? }) · signOut('this' | 'others' | 'all') · passkeysAvailable() · registerPasskey(label?) · signInWithPasskey() · requestPasswordReset({ identifier, kind? }) · resetPassword({ identifier, kind?, code, newPassword }) · changePassword({ currentPassword, newPassword }) · deleteAccount() · BillDogAuth.organizations — create, list, members, roles, invitations, domains.

Deleting an account

import { BillDogAuth, BillDogApiError } from '@billdog.io/react-native';

try {
  const { deleted } = await BillDogAuth.deleteAccount();
} catch (e) {
  if (e instanceof BillDogApiError && e.isReauthRequired) {
    // The sign-in is too old for this. Ask the person to sign in again,
    // then call deleteAccount() again.
  }
}

The local session is cleared only when the server confirmed the deletion. Any refusal leaves the person signed in and rejects with a BillDogApiError carrying the server's code.

What this SDK cannot do

  • Sign up into a project with bot protection or invite_only access. signUp takes no captcha or invitation token, and there is no config() to discover that one is required — the server answers 400. Use hosted sign-in for those projects until the parameters ship.

Supported Component Types

  • text - Text labels and headings
  • image - Images and icons
  • button - Call-to-action buttons
  • stack - Container for layout (vertical/horizontal)
  • package - Subscription package display
  • feature-list - List of features with checkmarks
  • price - Price display with currency
  • badge - Small labels (e.g., "Popular")
  • divider - Visual separator
  • spacer - Empty space for layout
  • icon - Icon components
  • video - Video players

Theme Support

Paywalls support custom themes with colors and fonts:

const config: PaywallConfiguration = {
  components: [...],
  theme: {
    colors: {
      primary: '#007AFF',
      background: '#FFFFFF',
      text: '#000000',
    },
    fonts: {
      body: 'System',
      heading: 'System-Bold',
    },
  },
};

Variable Substitution

Use {{variableName}} in text to inject dynamic values:

const config: PaywallConfiguration = {
  components: [
    {
      id: 'welcome',
      type: 'text',
      properties: {
        text: 'Welcome, {{userName}}!',
      },
    },
  ],
  variables: {
    userName: 'John Doe',
  },
};

License

MIT