@founderhq/journeys
v1.0.2
Published
Config-driven interactive journey/questionnaire engine for React
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 - self-hosted static Inter weights and italics as the deterministic default across browsers and native WebViews, with Lora Variable available when selected
Install
pnpm add @founderhq/journeysNative header layout
Journey and JourneyShell accept presentation="native" for an edge-to-edge
native WebView. This removes the web header's extra 32px top padding while
preserving all four device safe-area insets. The progress row reserves 80px on
the right for the host app's close control. Keep that control within the device
safe area. The default presentation="web" retains the existing web spacing.
The hosted native renderer selects this layout automatically for Swift/SPM, CocoaPods, Android Views/Compose, React Native, and Expo. Existing native SDKs receive this renderer update when a Journey is reopened; custom self-hosted renderers must also deploy the updated base package.
Basic Usage
import { Journey } from "@founderhq/journeys";
import "@founderhq/journeys/styles.css";
export function Example({
config,
}: {
config: import("@founderhq/journeys").JourneyConfig;
}) {
return (
<Journey
apiKey="fhq_pk_..."
journeyId="journey_123"
config={config}
storageKey="example-journey"
/>
);
}Importing styles.css also loads the bundled fonts; Journeys does not depend on
a third-party font request. Font files are split by character set and style, so
the browser downloads and caches only the faces used by the rendered Journey.
apiKey and journeyId are always required. Passing config only supplies local render data; the package still validates access with FounderHQ before rendering. Omit config to fetch the published config from FounderHQ:
<Journey apiKey="fhq_pk_..." journeyId="journey_123" />The browser package is public client code, so access enforcement happens on FounderHQ's server APIs: config fetch, validation, and capture all require a valid scoped publishable key.
Host App Identity
Pass identity when the host app already knows the respondent. This identity is
used only for safe FounderHQ Contact linking; it is not written into Journey
answers.
<Journey
apiKey="fhq_pk_..."
journeyId="journey_123"
identity={{
externalId: user.id,
email: user.email,
phone: user.phone,
}}
/>When any non-empty identity field is provided, it is authoritative for Contact
linking and overrides answer-derived email, phone, and external ID fields. The
respondent's typed answers are still stored in the response snapshot.
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="fhq_pk_..."
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="fhq_pk_..."
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
apiKey="fhq_pk_..."
journeyId="abc-123"
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.
Host URL Handling
Authored link buttons use normal browser navigation by default. Applications that own routing can intercept every link, including external links:
<Journey
apiKey="fhq_pk_..."
journeyId="journey_123"
onOpenURL={(url, external) => routeFromHost(url, { external })}
/>The native SDKs use this hook internally so link actions are handed to the Expo/React Native, iOS, or Android host instead of opening another WebView.
Capture Options
Capture is enabled by default after access validation succeeds. Pass capture={false} to opt out, or pass capture options to add custom context:
<Journey
apiKey="fhq_pk_..."
journeyId="journey_123"
capture={{ context: { app: { name: "web" } } }}
/>apiKey, journeyId, and the FounderHQ API URL are inherited from the root props and are not repeated inside capture.
Host integrations may supply revisionId and clientSessionId in capture
options. Queued events retain the identifiers and context from their original
presentation, including after restart or revision pruning. Retryable failures
retain the queue; custom capture transports may return a boolean or
{ ok: boolean, retryAfterMs?: number }.
Native preparation is managed by the platform SDK's persistent host. Internally,
JourneyProvider active={false} renders assets and layout while suppressing
experience activity until presentation. See the
mobile preparation guides.
Capture events are batched and persisted locally with a TTL. The runtime flushes
immediately for completion events, and browser embeds also try an exit-safe
sendBeacon/keepalive flush on visibilitychange and pagehide. Beacon
delivery is treated as best-effort; FounderHQ uses event IDs server-side so a
later retry can safely duplicate a queued event.
Config Shape
A journey config is centered around:
arcsLogical step groups and navigation order.stepsIndividual screens such assingle_select,multi_select,input,slider, andinfo_page.computedVariablesDerived values evaluated from the current answer set.
Computed variables and ${...} text templates use a restricted, side-effect-free
expression language. It supports property and array reads, arithmetic, comparisons,
boolean/nullish operators, ternaries, template strings, common string/array helpers,
and allowlisted built-ins such as Math. It does not execute arbitrary JavaScript or
functions supplied through journey data.
Development
pnpm --filter @founderhq/journeys dev
pnpm --filter @founderhq/journeys buildThe build emits ESM, CJS, type declarations, and dist/styles.css.
Mobile SDKs
Journeys uses the same authored block runtime on web and native platforms:
- Expo and bare React Native:
@founderhq/journeys-react-native - SwiftUI and UIKit:
FounderHQJourneys - Android Views:
founderhq-journeys - Jetpack Compose:
founderhq-journeys-compose
See the README in each platform package for installation and usage. The shared
architecture and security model are documented in
docs/journeys-mobile-sdk-plan.md in the FounderHQ repository.
Release notes
1.0.2
- Icons show again in the timeline, callout, card, metric, table and before-after blocks. From 0.6.0 to 1.0.1 they rendered with no stroke and were invisible.
- A glow spot (
blobs) with a percentagesizestays round: the size is a share of the screen's longer side, as the default60vmax. Before, "60%" was 60% of the width and of the height, which cut the glow off at its sides on a phone.
1.0.1
- The package now ships its MIT licence: a
LICENSEfile and thelicensefield. - Updated dependencies, among them
motion14 and@lottiefiles/dotlottie-react0.21.
0.8.1
Native Android hosts can provide measured safe-area insets. The renderer keeps progress, navigation, and overlays clear of system bars on older WebViews while preserving edge-to-edge backgrounds and the existing iOS CSS inset behavior.
0.8.0
- Journey API requests and renderer pages use
https://app.getfounderhq.com. The analytics ingest host is reserved for Events.
