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

@founderhq/journeys

v0.3.67

Published

Config-driven interactive journey/questionnaire engine for React

Downloads

1,065

Readme

@founderhq/journeys

@founderhq/journeys is a config-driven React runtime for interactive onboarding and questionnaire flows.

What It Includes

  • journey runtime with forward/back navigation
  • conditional routing between steps
  • computed variables derived from prior answers
  • block-based informational pages
  • reusable styles shipped as styles.css

Install

pnpm add @founderhq/journeys

Basic Usage

import { Journey } from "@founderhq/journeys";
import "@founderhq/journeys/styles.css";

export function Example({ config }: { config: import("@founderhq/journeys").JourneyConfig }) {
  return <Journey config={config} storageKey="example-journey" />;
}

You can also fetch a journey remotely by passing apiKey and journeyId instead of an inline config.

Injecting Runtime Data (initialAnswers)

Pass initialAnswers to seed the answer state at first render. Useful when journey config references runtime data via templates — e.g. live pricing plans from StoreKit, RevenueCat, or your own API:

<Journey
  apiKey="fos_..."
  journeyId="abc-123"
  initialAnswers={{
    pricingPlans: [
      {
        id: "pro_monthly",
        name: "Pro",
        price: { amount: 29, currency: "USD", period: "month", trial: { days: 7 } },
        metadata: { stripePriceId: "price_xxx" },
      },
    ],
  }}
  onEvent={(event) => {
    if (event.type === "purchase_intent") {
      // event.plan.metadata.stripePriceId → kick off Stripe checkout
    }
  }}
/>

In the journey config, reference the injected value with a whole-value template:

{ type: "pricing_plans", props: { variable: "selectedPlan", plans: "${pricingPlans}" } }

Keys declared in computedVariables are ignored in initialAnswers; computed values always come from their formulas.

Injecting Runtime Options (initialOptions)

Pass initialOptions to override option lists at render time without storing large dynamic lists in the journey config. Keys are answer keys: top-level steps use step.variable ?? step.id, and single_select / multi_select blocks use props.variable.

<Journey
  apiKey="fos_..."
  journeyId="abc-123"
  initialOptions={{
    country: countries.map((country) => ({
      id: country.code,
      label: country.name,
    })),
  }}
/>

When a key is present in initialOptions, that list is used even if it is empty. When no key is present, the inspector-configured options remain the fallback.

Event Payloads

onEvent payloads keep user-provided answers and computed values separate. step_submit and navigate events include the rendered step config, but option arrays are omitted from those event configs so large initialOptions lists are not copied into every payload.

onEvent={(event) => {
  // Raw/persisted answers only.
  event.answers;

  // Derived values from config.computedVariables.
  event.computedVariables;
}}

Pricing Plan Templates

When a user selects a plan, the block stores a flattened snapshot in answers[variable]. Templates anywhere downstream can reach into it with dotted paths:

You're subscribing to ${selectedPlan.name} — ${selectedPlan.amount} ${selectedPlan.currency}/${selectedPlan.period}.
${selectedPlan.trialDays} day free trial. Was ${selectedPlan.originalAmount}.

Available paths: id, name, amount, currency, period, originalAmount, perUnitLabel, display, trialDays, introOffer, description, badge, icon, features, metadata.

Equality conditions on the variable continue to match the plan id ({ op: "equals", value: "pro_monthly" }).

Per-Plan Templates (currentPlan)

String fields anywhere inside a plan support templates, with currentPlan exposing this card's flattened plan fields (same shape as selectedPlan):

{
  id: "pro_monthly",
  name: "Pro",
  price: { amount: 29, currency: "USD", period: "month", trial: { days: 7 } },
  badge: "${currentPlan.trialDays} Days Free",
  subtext: "${currentPlan.trialDays} day free trial, then ${currentPlan.amount} ${currentPlan.currency}/${currentPlan.period}",
}

subtext is rendered as a small line under the price. Trial and intro-offer lines are no longer auto-rendered — set subtext explicitly if you want them.

Optional Discount Codes

Discount codes are opened from a normal button action, so the Journey author controls whether the UI appears and where the trigger lives:

{
  type: "button",
  props: {
    label: "Have a coupon?",
    action: {
      type: "open_discount_code",
      variable: "pricingDiscount",
      planVariable: "selectedPlan",
    },
  },
}

Pass onDiscountCodeApply to validate and reprice in the consumer app:

<Journey
  config={config}
  onDiscountCodeApply={async ({ code, plan }) => {
    const result = await validateCoupon(code, plan?.id);
    return result.valid
      ? {
          valid: true,
          code,
          planId: plan?.id,
          message: "Discount applied",
          pricing: {
            total: result.total,
            currency: result.currency,
            originalAmount: result.originalTotal,
          },
          metadata: result.metadata,
        }
      : { valid: false, reason: result.reason };
  }}
/>

Set pricing_plans.props.discountVariable to let cards replace the visible non-strike price from pricing.display, pricing.amount, or pricing.total. Set purchase.action.discountVariable to include the full applied discount answer in purchase_intent. Journeys never calculates billing amounts or provider rules; those belong in the consumer callback and metadata.

Config Shape

A journey config is centered around:

  • arcs Logical step groups and navigation order.
  • steps Individual screens such as single_select, multi_select, input, slider, and info_page.
  • computedVariables Derived values evaluated from the current answer set.

Development

pnpm --filter @founderhq/journeys dev
pnpm --filter @founderhq/journeys build

The build emits ESM, CJS, type declarations, and dist/styles.css.