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

@growthcat/web

v0.0.4

Published

GrowthCat Web SDK — referral codes, ads, attribution, and feedback for React and web apps.

Readme

GrowthCat Web SDK

GrowthCat is a TypeScript/React SDK for referral-code redemption, in-app ads, attribution, and user feedback on the web.

Core capabilities:

  • Referral Redemption — validate promo/referral codes with full funnel analytics.
  • Ads — fetch, cache, and track ads across banner and interstitial formats.
  • Custom Ad Rendering — get the raw AdObject via useAd() and build your own design with full freedom.
  • Attribution — generate share links, resolve deferred attribution, confirm tokens, and track events.
  • Feedback — let users submit ideas, bugs, and feedback; vote on public items; stay anonymous or identify.

Table of Contents

  1. Installation
  2. Initialization
  3. Referral Redemption
  4. Ads
  5. Attribution
  6. Feedback
  7. Error Handling
  8. SDK Config Flags
  9. Full API Reference

Installation

npm install @growthcat/web
# or
yarn add @growthcat/web

React components and hooks are in the /react subpath (React is a peer dependency):

npm install @growthcat/web react react-dom

Initialization

Call GrowthCat.initialize() once, as early as possible — ideally in your root App component or main.tsx.

import { GrowthCat } from "@growthcat/web";

GrowthCat.initialize({
  apiKey: "gc_live_your_key_here",
  logsEnabled: process.env.NODE_ENV !== "production",
});

What happens on initialize

  1. The SDK stores your API key and resolves the workspace (sandbox or live).
  2. A background task calls GET /v1/sdk/config to fetch the bootstrap:
    • Feature flags (showCodeRedeemUI, allowCodeRedeemExecution)
    • Ads configuration (ads_enabled, cache TTL)
  3. The AdService is configured with the returned ads settings.
  4. All subsequent calls to GrowthCat.shared are ready.

Parameters

| Parameter | Type | Default | Description | |---|---|---|---| | apiKey | string | required | Your GrowthCat SDK key from the dashboard. | | environmentMode | "automatic" \| "production" | "automatic" | Controls the workspace header (sandbox vs live). | | logsEnabled | boolean | false | Emits debug logs to the console. | | baseUrl | string? | https://api.growthcat.dev | Override backend URL for QA environments. |

Handle attribution on page load

If your site receives GrowthCat attribution links, call this right after initialize:

GrowthCat.initialize({ apiKey: "..." });
GrowthCat.handleCurrentUrl(); // reads window.location.href and claims any gc_token

Referral Redemption

Option A: Built-in form

Drop in <GrowthCatReferralForm> and attach a success callback:

import { GrowthCatReferralForm } from "@growthcat/web/react";

function SettingsPage() {
  return (
    <GrowthCatReferralForm
      onSuccess={(result) => {
        console.log("Applied:", result.normalizedCode);
        // Unlock access here — this is the safe point.
      }}
      onError={(error) => console.error(error.message)}
      theme={{ accentColor: "#FF6B00" }}
      strings={{ title: "Got a promo code?", buttonText: "Redeem" }}
    />
  );
}

Option B: useReferral hook (custom UI)

import { useReferral } from "@growthcat/web/react";

function CustomReferralForm() {
  const [code, setCode] = useState("");
  const { isLoading, error, result, validateCode } = useReferral({
    onSuccess: (r) => grantAccess(r.normalizedCode),
  });

  return (
    <form onSubmit={(e) => { e.preventDefault(); validateCode(code); }}>
      <input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Enter code" />
      <button disabled={isLoading}>{isLoading ? "Verifying…" : "Apply"}</button>
      {error && <p style={{ color: "red" }}>{error.message}</p>}
      {result && <p>✓ {result.normalizedCode} applied!</p>}
    </form>
  );
}

Option C: Headless API

const result = await GrowthCat.shared.validateReferralCode("MARCO20");
console.log(result.normalizedCode, result.campaign.name);

Analytics events

Track funnel events tied to a code, using the same session across all calls:

const sessionId = GrowthCat.makeSessionId();
const context = { sessionId, source: "creator_profile" };

// 1. User saw the code link
await GrowthCat.shared.recordReferralClick("MARCO20", context);

// 2. User validated the code
const result = await GrowthCat.shared.validateReferralCode("MARCO20", context);

// 3. User reached the paywall
await GrowthCat.shared.recordAnalyticsEvent("paywall_viewed", {
  code: result.normalizedCode,
  context,
  properties: { paywall_variant: "annual_default" },
});

Available event names: "paywall_viewed", "checkout_started", "checkout_cancelled", "purchase_sdk_completed".


Ads

How ads work

initialize()
    └─▶ GET /v1/sdk/config          ← bootstrap: ads_enabled, TTL
           │
           ▼
loadAd({ placementKey })            ← named placement slot
    └─▶ GET /v1/ads/catalog?placement_key=…

— or —

loadAd({ format: "banner" })        ← automatic format selection
    └─▶ GET /v1/ads/serve?format=banner|interstitial
           │
           ├─▶ cache response (TTL from response)
           │
           ▼
render ad with <GrowthCatAdBanner> or your own component (useAd)
    └─▶ POST /v1/ads/events/batch   ← impression, click, video events
           │
           ▼  (rewarded only)
POST /v1/ads/reward/validate        ← server confirms reward

Ad formats

| AdFormat | Description | Renders as | |---|---|---| | "banner" | Fixed-height strip, full width | Inline component | | "interstitial" | Full-screen modal (image, video, rewarded) | Overlay modal |

Banner ad

import { GrowthCatAdBanner } from "@growthcat/web/react";

function HomePage() {
  return (
    <div>
      {/* Your page content */}
      <GrowthCatAdBanner
        placementKey="home_banner"
        appUserId="user_123"
        sessionId={GrowthCat.makeSessionId()}
        minHeight={60}
        onTap={() => console.log("Banner tapped")}
      />
    </div>
  );
}

<GrowthCatAdBanner> fires an impression event automatically once 50%+ of the banner is in the viewport.

You can also use the format-based initializer:

<GrowthCatAdBanner format="banner" appUserId="user_123" />

Interstitial ad

import { GrowthCatAdInterstitial } from "@growthcat/web/react";

function LevelCompleteScreen() {
  const [showAd, setShowAd] = useState(false);

  return (
    <>
      <button onClick={() => setShowAd(true)}>Continue</button>
      <GrowthCatAdInterstitial
        placementKey="level_complete_interstitial"
        appUserId="user_123"
        isOpen={showAd}
        onDismiss={() => setShowAd(false)}
      />
    </>
  );
}

For rewarded interstitials:

<GrowthCatAdInterstitial
  placementKey="rewarded_coins"
  appUserId="user_123"  // REQUIRED for reward attribution
  isOpen={showRewardedAd}
  onDismiss={() => setShowRewardedAd(false)}
  onReward={(response) => {
    // Only called when reward_validated === true from the server.
    addCoins(response.reward?.amount ?? 0);
    setShowRewardedAd(false);
  }}
/>

Custom ad rendering with useAd()

useAd() is the core add-on for custom UI. It gives you the raw AdObjectcreative.headline, creative.body, creative.ctaText, creative.publicAssetUrl, and creative.layout — so you can build the banner or interstitial exactly how you want.

The SDK still handles impression/click tracking via the returned trackEvent function.

import { useAd } from "@growthcat/web/react";

function MyCustomBanner({ placementKey }: { placementKey: string }) {
  const { ad, state, trackEvent } = useAd({
    placementKey,
    trackImpression: true,  // auto-fires impression when state becomes "ready"
  });

  if (state === "loading") return <div style={{ height: 60 }} />;
  if (!ad) return null;  // no fill

  const { creative } = ad;
  const layout = creative.layout?.banner;  // server-configured layout data

  return (
    <div
      style={{
        background: layout?.backgroundColor ?? "#F0F4FF",
        borderRadius: layout?.borderRadius ?? 12,
        padding: `${layout?.padding?.vertical ?? 12}px ${layout?.padding?.horizontal ?? 16}px`,
        display: "flex",
        alignItems: "center",
        gap: layout?.gap ?? 12,
        cursor: "pointer",
      }}
      onClick={() => {
        trackEvent("click");
        if (creative.destinationUrl) window.open(creative.destinationUrl, "_blank");
      }}
    >
      {/* Your design — full freedom */}
      {creative.publicAssetUrl && (
        <img src={creative.publicAssetUrl} style={{ width: 48, height: 48, borderRadius: 8 }} />
      )}
      <div>
        <strong style={{ color: layout?.title?.color }}>{creative.headline}</strong>
        <p>{creative.body}</p>
      </div>
      <button>{creative.ctaText}</button>
    </div>
  );
}

useAd() options

| Option | Type | Description | |---|---|---| | placementKey | string | Named placement key from your dashboard. | | format | "banner" \| "interstitial" | Format-based loading via /v1/ads/serve. | | appUserId | string? | Your user identifier — required for rewarded ads. | | sessionId | string? | From GrowthCat.makeSessionId(). Correlates events. | | trackImpression | boolean | Auto-fire impression when state becomes "ready". Default true. | | onLoad | (ad) => void | Called when an ad loads. | | onNoFill | () => void | Called when no ad is available. | | onError | (error) => void | Called on load failure. |

useAd() return value

| Field | Type | Description | |---|---|---| | ad | AdObject \| null | The raw ad object — null until loaded or when no fill. | | state | "idle" \| "loading" \| "ready" \| "no_fill" \| "error" | Load state. | | error | GrowthCatError \| null | Error if state is "error". | | trackEvent | (name, metadata?) => void | Fire a tracking event for this ad. | | reload | () => void | Reload the ad (e.g. after dismissal). |

The AdObject shape

interface AdObject {
  placement?: {
    id: string;
    format: "banner" | "interstitial";
    rewardEnabled: boolean;
    rewardName?: string;
    rewardAmount?: number;
    minimumViewSeconds?: number;
    isSkippable: boolean;
    skippableAfterSeconds?: number;
  };
  campaign: {
    id: string;
    mode: string;
    objective: string;
    priorityWeight: number;
  };
  creative: {
    id: string;
    creativeType: string;            // "image" | "video" | "app_store"
    publicAssetUrl?: string;         // Remote URL for the image or video asset
    layout?: AdCreativeLayout;       // Server-configured visual layout data
    headline?: string;               // Ad headline text
    body?: string;                   // Ad body text
    ctaText?: string;                // Call-to-action button label
    destinationType: string;
    destinationUrl?: string;         // Where to send the user on tap
  };
  tracking: {
    allocationId?: string;
    token: string;
  };
  closePolicy?: {
    isSkippable?: boolean;
    skippableAfterSeconds?: number;
    rewardGrantAfterSeconds?: number;
  };
}

AdCreativeLayout — the design data

The creative.layout object contains server-configured styling so the same ad can look different per placement:

interface AdCreativeLayout {
  renderVersion?: number;
  background?: {
    type: string;          // "solid" | "gradient"
    color?: string;        // hex for solid
    colors?: string[];     // array for gradient
    direction?: string;    // e.g. "to bottom right"
  };
  banner?: {
    backgroundColor?: string;
    borderRadius?: number;
    padding?: { horizontal?: number; vertical?: number };
    gap?: number;
    adBadge?: { text?: string; backgroundColor?: string; textColor?: string; borderRadius?: number };
    icon?: { size?: number; borderRadius?: number; backgroundColor?: string; borderColor?: string };
    title?: { color?: string; fontWeight?: string; lineLimit?: number };
    ctaButton?: { bgColor?: string; textColor?: string; borderRadius?: number; style?: string };
  };
  overlay?: {             // For interstitial layouts
    layout?: "scrim" | "panel" | "floating" | "cta_only" | "asset_embedded";
    position?: string;
    scrimOpacity?: number;
    scrimColor?: string;
    panelBgColor?: string;
    panelBorderRadius?: number;
    textColor?: string;
    imageFit?: "cover" | "contain" | "fill";
    ctaBottomPadding?: number;
    ctaButton?: { bgColor?: string; textColor?: string; borderRadius?: number };
    title?: { color?: string; size?: string; weight?: string };
  };
  ctaButton?: { bgColor?: string; textColor?: string; borderRadius?: number; style?: string };
}

Manual ad loading and event tracking

// Check if an ad is available before showing a "Watch Ad" button.
const ad = await GrowthCat.shared.loadAd({ placementKey: "rewarded_coins" });
if (ad) showWatchAdButton();

// Fire events manually for a custom renderer.
GrowthCat.shared.trackAdEvent("impression", ad, {
  format: "banner",
  placementKey: "home_banner",
  appUserId: "user_123",
  sessionId,
});

GrowthCat.shared.trackAdEvent("click", ad, {
  format: "banner",
  placementKey: "home_banner",
  appUserId: "user_123",
});

// Video progress (pass progress_pct in metadata).
GrowthCat.shared.trackAdEvent("video_progress", ad, {
  format: "interstitial",
  placementKey: "video_placement",
  metadata: { progress_pct: "50" },
});

Available event names: "impression", "click", "video_start", "video_progress", "video_complete", "ad_closed", "ad_reported".

Reward validation

const response = await GrowthCat.shared.validateAdReward(ad, "user_123", {
  sessionId,
  viewedSeconds: 30,
  completed: true,
});

if (response.rewardValidated) {
  grantCoins(response.reward?.amount ?? 0);
}
// Never grant the reward if rewardValidated is false or the call throws.

Offline behavior

| Scenario | What happens | |---|---| | Ad catalog fetch fails (offline) | Returns cached entry if TTL has not expired. | | Event tracking (offline) | Events are persisted to localStorage and replayed automatically on reconnect. | | Reward validation (offline) | The SDK throws — no reward is granted. | | App comes back online | window.online event triggers automatic queue flush. |

Prefetching

// Call on route change or page visibility change to warm the cache.
GrowthCat.shared.prefetchAdCatalogs(["home_banner", "level_interstitial"]);

// Flush events before the page unloads.
window.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    void GrowthCat.shared.flushAdEvents();
  }
});

Attribution

// Set user ID for attribution.
GrowthCat.setAppUserId("user_123");

// Generate a share link for a campaign.
const link = await GrowthCat.shared.generateShareLink({
  campaignKey: "influencer_spring",
  deepLinkValue: "/welcome",
  metadata: { source: "instagram" },
});
console.log(link.publicUrl);  // Share this URL

// Handle a GrowthCat link when a user opens it.
const assignment = await GrowthCat.shared.handleLink(url);
if (assignment?.deepLinkValue) {
  router.push(assignment.deepLinkValue);
}

// Resolve deferred attribution on first load (fingerprinting).
const result = await GrowthCat.shared.resolveAttribution();
if (result?.matched) {
  console.log("Referred by campaign:", result.campaignKey);
}

// Confirm a token explicitly (e.g., from a URL param).
const token = new URLSearchParams(window.location.search).get("gc_token");
if (token) {
  await GrowthCat.shared.confirmAttribution(token);
}

// Track a custom event.
await GrowthCat.shared.track("upgrade_clicked", { plan: "annual" });

// Check what rewards the user has earned.
const { rewards } = await GrowthCat.shared.rewards();
const hasPremium = rewards.containsFeature("premium_access");

Feedback

import { GrowthCat } from "@growthcat/web";

// Configure feedback (call once after initialize).
GrowthCat.shared.configureFeedback({
  user: { id: "user_123", email: "[email protected]", name: "Jane" },
  theme: { accentColor: "#0A84FF" },
  strings: { title: "Share your thoughts" },
});

// The built-in board follows the user's system color scheme by default.
// You can also force a mode or customize separate dark colors.
GrowthCat.shared.configureFeedback({
  theme: {
    mode: "system", // "light" | "dark" | "system"
    accentColor: "#0A84FF",
    buttonTextColor: "#FFFFFF",
    overlayColor: "rgba(0,0,0,0.4)",
    inputBackgroundColor: "#FFFFFF",
    selectedControlBackgroundColor: "rgba(10,132,255,0.1)",
    dark: {
      backgroundColor: "#111113",
      cardColor: "#1C1C1E",
      primaryTextColor: "#F5F5F7",
      secondaryTextColor: "#A1A1AA",
      borderColor: "#2C2C2E",
      overlayColor: "rgba(0,0,0,0.55)",
      inputBackgroundColor: "#111113",
      selectedControlBackgroundColor: "rgba(10,132,255,0.18)",
    },
  },
});

// Submit feedback programmatically.
const result = await GrowthCat.shared.submitFeedback({
  type: "bug",
  title: "Export button crashes",
  body: "When I tap Export from the reports screen the app freezes.",
  metadata: { screen: "reports" },
});

// Fetch the public board.
const items = await GrowthCat.shared.fetchFeedbackBoard("idea");

// Vote and unvote.
await GrowthCat.shared.voteFeedbackItem(items[0].itemId);
await GrowthCat.shared.unvoteFeedbackItem(items[0].itemId);

// Sign out.
GrowthCat.shared.clearFeedbackUser();

Built-in feedback board component

import { GrowthCatFeedbackBoard } from "@growthcat/web/react";

function FeedbackPage() {
  return (
    <GrowthCatFeedbackBoard
      defaultType="idea"
      style={{ minHeight: "100vh" }}
    />
  );
}

Custom board with useFeedbackBoard

import { useFeedbackBoard } from "@growthcat/web/react";

function MyBoard() {
  const { items, isLoading, vote, unvote } = useFeedbackBoard({ autoLoad: true });

  return (
    <ul>
      {items.map((item) => (
        <li key={item.itemId}>
          <button onClick={() => vote(item.itemId)}>▲ {item.voteCount}</button>
          <strong>{item.title}</strong>
        </li>
      ))}
    </ul>
  );
}

Error Handling

All async SDK methods throw GrowthCatError:

import { GrowthCatError } from "@growthcat/web";

try {
  await GrowthCat.shared.validateReferralCode("BAD_CODE");
} catch (err) {
  if (err instanceof GrowthCatError) {
    switch (err.code) {
      case "invalid_code":
        showError("That code doesn't exist. Try again.");
        break;
      case "unauthorized":
        console.error("Check your SDK key.");
        break;
      case "rate_limited":
        console.log("Retry after:", err.retryAfter, "seconds");
        break;
      case "network":
        showError("No internet connection.");
        break;
      default:
        showError(err.message);
    }
  }
}

Error codes

| Code | Meaning | |---|---| | not_initialized | GrowthCat.initialize() not called. | | missing_app_user_id | setAppUserId() needed for this operation. | | invalid_code | Referral code not found or inactive. | | app_setup_incomplete | Backend configuration is missing. Check err.requirements. | | unauthorized | Invalid SDK key. | | rate_limited | 429 response. Check err.retryAfter. | | network | No connectivity or fetch failed. | | server | 5xx or unexpected status. Check err.statusCode. | | unknown | Unexpected error. |


SDK Config Flags

The bootstrap from GET /v1/sdk/config exposes flags you can read:

const client = GrowthCat.shared;

// Referral flags
if (client.sdkConfig?.showCodeRedeemUI) showRedeemButton();
if (client.sdkConfig?.allowCodeRedeemExecution) allowUserToStart();

// Ads flag
if (client.adsConfig?.adsEnabled) prefetchAds();

// Manually refresh (e.g., after sign-in or page focus).
const config = await GrowthCat.shared.refreshSDKConfig();

Full API Reference

GrowthCat namespace

GrowthCat.initialize(options: GrowthCatInitOptions): void
GrowthCat.isConfigured: boolean
GrowthCat.shared: GrowthCatClient
GrowthCat.makeSessionId(): string
GrowthCat.setAppUserId(userId: string): void
GrowthCat.handleCurrentUrl(): void

GrowthCatClient

// Bootstrap
client.sdkConfig: GrowthCatSDKConfig | null
client.sdkBootstrap: GrowthCatSDKBootstrap | null
client.adsConfig: GrowthCatSDKAdsConfig | null
client.refreshSDKConfig(): Promise<GrowthCatSDKConfig>
client.refreshSDKBootstrap(): Promise<GrowthCatSDKBootstrap>

// Referral
client.validateReferralCode(code, context?): Promise<ReferralRedemptionResult>
client.recordReferralClick(code, context?): Promise<void>
client.recordAnalyticsEvent(name, options?): Promise<void>

// Ads
client.loadAd({ placementKey } | { format }): Promise<AdObject | null>
client.trackAdEvent(name, ad, options): void
client.validateAdReward(ad, appUserId, options?): Promise<AdRewardValidationResponse>
client.prefetchAdCatalogs(placementKeys): void
client.flushAdEvents(): Promise<void>

// Attribution
client.setAppUserId(userId): void
client.generateShareLink(options): Promise<AttributionLink>
client.handleLink(url): Promise<AttributionAssignment | null>
client.resolveAttribution(sessionId?): Promise<AttributionResolveResult | null>
client.confirmAttribution(token, sessionId?): Promise<AttributionAssignment | null>
client.track(eventName, properties?): Promise<void>
client.rewards(): Promise<GrowthCatRewards>

// Feedback
client.configureFeedback(options): void
client.identifyFeedbackUser(user): void
client.clearFeedbackUser(): void
client.setFeedbackMetadata(metadata): void
client.submitFeedback(submission): Promise<FeedbackSubmitResult>
client.fetchFeedbackBoard(type?): Promise<FeedbackBoardItem[]>
client.voteFeedbackItem(itemId): Promise<FeedbackVoteResult>
client.unvoteFeedbackItem(itemId): Promise<FeedbackVoteResult>
client.feedbackAnonymousId: string

React exports (@growthcat/web/react)

// Hooks
useAd(options): UseAdResult
useReferral(options?): UseReferralResult
useFeedbackBoard(options?): UseFeedbackBoardResult
useFeedbackSubmit(): UseFeedbackSubmitResult

// Components
<GrowthCatAdBanner placementKey|format appUserId? sessionId? minHeight? onTap? />
<GrowthCatAdInterstitial placementKey|format appUserId? isOpen onDismiss onReward? />
<GrowthCatReferralForm strings? theme? context? onSuccess? onError? />
<GrowthCatFeedbackBoard defaultType? />

iOS SDK

This package is the web equivalent of the GrowthCat iOS SDK (Swift Package).