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

@fictura/sdk

v0.13.0

Published

Fictura mobile SDK — analytics, server-driven paywalls, experiments, cancel surveys. One provider, zero re-submissions.

Downloads

2,111

Readme

@fictura/sdk

The Fictura mobile SDK for Expo/React Native apps. One provider gives you analytics, churn tracking, cancel surveys, server-driven paywalls, and A/B experiments — all controlled from the dashboard, with zero app updates to change any of it.

Apps built inside the Fictura builder ship with this pre-wired. Existing apps integrate in ~10 minutes:

Install (existing Expo app)

npm install @fictura/sdk
npx expo install expo-secure-store expo-application   # reinstall-proof device id (optional but recommended)

Wire it up

// app/_layout.tsx — module scope, before components render
import { Growth, GrowthProvider } from "@fictura/sdk";

Growth.init({
  apiBase: process.env.EXPO_PUBLIC_GROWTH_API_BASE ?? "",
  apiKey: process.env.EXPO_PUBLIC_GROWTH_API_KEY ?? "", // Dashboard → Setup
  // If you use RevenueCat (recommended):
  getRevenueCatId: async () => (await Purchases.getCustomerInfo()).originalAppUserId,
  purchases: revenueCatAdapter, // see below
});

export default function Layout() {
  return <GrowthProvider>{/* your app */}</GrowthProvider>;
}

Mount a paywall slot

import { GrowthPaywall } from "@fictura/sdk";

<GrowthPaywall
  placement="default"
  onClose={() => router.back()}
  onPurchaseComplete={() => router.replace("/")}
/>;

Which paywall renders there — template, copy, pricing presentation, experiment variant — is decided on the dashboard. Prices always come from the store at runtime; configs cannot contain price strings.

Mount an onboarding slot

import { GrowthOnboarding } from "@fictura/sdk";

<GrowthOnboarding
  placement="default"
  assets={ONBOARDING_TEMPLATE_ASSETS}     // your bundled artwork (see below)
  onComplete={() => router.replace("/home")}
  fallback={<YourHardcodedOnboarding />}   // shown when no live flow is published
/>;

Which onboarding renders — template, copy, which steps run, experiment variant — is decided on the dashboard. Built-in templates (house_ai, temp4, temp5, temp6) ship in the SDK and resolve the moment you import the mount point; a founder picking one on the dashboard needs no app wiring.

Imagery is host-provided, not shipped in the SDK. Template code lives in the SDK; the photos each template draws are passed through assets (bundled require()s, keyed by the names the template reads). This keeps the SDK light, lets each app brand its own artwork, avoids shipping licensed images inside a distributed package — and bundling (vs a CDN) keeps the first onboarding screen instant, with no network wait.

Runs on Expo AND bare React Native CLI

The onboarding templates render through a small UI adapter instead of importing Expo packages directly, so they work in both environments.

Required in both: react-native-svg (the default gradient and the icons use it).

npm install react-native-svg

Expo app — inject expo-image (cached, top-crop) and expo-linear-gradient for the best result:

import { setFicturaUI } from "@fictura/sdk";
import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";

setFicturaUI({ Image, LinearGradient }); // module scope, before render

Bare React Native CLI app — do nothing. The SDK falls back to RN's built-in Image + a react-native-svg gradient, which render everywhere. (You may inject react-native-fast-image / react-native-linear-gradient the same way if you prefer.)

Why an adapter and not a try/catch require: Metro resolves every require string at build time, so a bare-RN app bundling require('expo-image') would fail to build even inside a try/catch. The SDK references no Expo module — the Expo host imports its own copy and injects it, and Metro resolves that in the host's context, where it's installed.

RevenueCat adapter (~20 lines)

import Purchases from "react-native-purchases";
import type { PurchasesAdapter } from "@fictura/sdk";

export const revenueCatAdapter: PurchasesAdapter = {
  async getPackages(offeringId) {
    const offerings = await Purchases.getOfferings();
    const offering = offeringId ? offerings.all[offeringId] : offerings.current;
    return (offering?.availablePackages ?? []).map((p) => ({
      id: p.identifier,
      product_id: p.product.identifier,
      price_string: p.product.priceString,
      price_micros: Math.round(p.product.price * 1_000_000),
      currency: p.product.currencyCode ?? "USD",
      period:
        p.packageType === "MONTHLY" ? "monthly"
        : p.packageType === "ANNUAL" ? "annual"
        : p.packageType === "WEEKLY" ? "weekly"
        : p.packageType === "LIFETIME" ? "lifetime"
        : "other",
      trial_days: p.product.introPrice?.periodUnit === "DAY"
        ? p.product.introPrice.periodNumberOfUnits
        : p.product.introPrice ? 7 : null,
    }));
  },
  async purchase(packageId, offeringId) {
    try {
      const offerings = await Purchases.getOfferings();
      const offering = offeringId ? offerings.all[offeringId] : offerings.current;
      const pkg = offering?.availablePackages.find((p) => p.identifier === packageId);
      if (!pkg) return { success: false, error: "package_not_found" };
      await Purchases.purchasePackage(pkg);
      return { success: true };
    } catch (e: any) {
      return e?.userCancelled ? { success: false, cancelled: true } : { success: false, error: String(e) };
    }
  },
  async restore() {
    try { await Purchases.restorePurchases(); return true; } catch { return false; }
  },
};

In previews / development without RevenueCat, pass mockPurchasesAdapter.

Webview paywalls that BUY — binding buttons to YOUR packages

A served (HTML) paywall renders in a webview and can trigger real purchases through the same adapter above — the page asks, the native side buys. Two rules:

  • Prices are never in the HTML. The SDK injects your live store packages as window.__FICTURA__.packages; the page reads them. A config that hardcoded a price is rejected server-side.
  • Buying is native. A button calls window.__FICTURA__.purchase(id); the SDK routes it to your PurchasesAdapter.purchase(id) → your RevenueCat.

Bind a button — by PERIOD (recommended)

Tag any buy button. Bind by period (weekly / monthly / annual / lifetime) so it works regardless of your identifier names — you never hardcode $rc_monthly, and another app's custom identifiers still resolve:

<button data-fictura-plan="annual"><span data-fictura-price></span></button>
<button data-fictura-plan="weekly"><span data-fictura-price></span></button>

An exact package id also works: data-fictura-plan="$rc_monthly" (id match first, then period). A button whose plan matches no package is auto-hidden — never a dead button. Drop this universal binder once, anywhere in the page:

<script>
(function () {
  function wire() {
    var F = window.__FICTURA__ || {}, pkgs = F.packages || [];
    function find(sel) {
      return pkgs.filter(function (p) { return p.id === sel; })[0]
          || pkgs.filter(function (p) { return p.period === sel; })[0];
    }
    document.querySelectorAll('[data-fictura-plan]').forEach(function (btn) {
      var pkg = find(btn.getAttribute('data-fictura-plan'));
      if (!pkg) { btn.style.display = 'none'; return; }
      var el = btn.querySelector('[data-fictura-price]');
      if (el) el.textContent = pkg.price_string;   // live, per-storefront price
      btn.addEventListener('click', function () { F.purchase(pkg.id); });
    });
  }
  document.readyState === 'loading'
    ? document.addEventListener('DOMContentLoaded', wire) : wire();
})();
</script>

Also available: window.__FICTURA__.restore(), .close(), .track(event, props).

Which offering/packages a paywall uses

Set per-paywall on the dashboard (paywall config), not in code:

  • offering_id — which of your RevenueCat offerings (blank = your current offering)
  • package_selection"all", or specific package identifiers to show
  • highlight_package — which to pre-select

These are just string references to your account — Fictura never holds your RevenueCat key. Package data (prices, products) always comes from your app's adapter at runtime.

What YOU own — client checklist

Fictura and the dashboard cannot do these for you: they touch your store account or your app code.

  • [ ] Your products — create weekly/monthly/annual products in App Store Connect / Play Console, put them in a RevenueCat Offering, define your entitlement.
  • [ ] RevenueCat → Fictura webhook — paste your app's webhook URL (Dashboard → Setup) into RevenueCat so revenue events + the cancel survey fire.
  • [ ] Wire PurchasesAdapter to your own RevenueCat (~20 lines above). This is what makes every paywall show your packages and buy through your account.
  • [ ] Growth.init with your apiBase + apiKey (Dashboard → Setup).
  • [ ] Mount pointsGrowthPaywall / GrowthOnboarding with a fallback (your built-in screen when nothing is live) and, for native templates, assets.
  • [ ] Native template imagery — pass your artwork via assets (SDK ships code, not photos).
  • [ ] Legal URLs — set legal: { termsUrl, privacyUrl } in Growth.init. Paywall "Terms of Service" / "Privacy Policy" links open these (App Store Review 3.1.2 needs them wherever a subscription sells). The SDK ships no client's URLs.
  • [ ] Webview paywall buttons — tag buy buttons with data-fictura-plan + drop the universal binder (above). Bind by period so identifier names don't matter.
  • [ ] Attribution (optional) — install expo-linking; register onConversionData / onDeepLink at module scope; observe only, don't fight your existing router.
  • [ ] Push (optional) — call registerForPush() after the user grants permission.

What's automatic once wired

  • app_open, screen views + dwell, auto-named taps, batched delivery
  • Identity resolution (device / RevenueCat / account → one user)
  • Cancel survey after trial cancellation (armed server-side)
  • Live paywalls + experiment assignment per placement
  • Remote kill-switches: every tool and every event can be disabled from the dashboard — the SDK obeys on next launch/foreground, fail-open on network errors