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

@codyzk/onboarding

v0.0.3

Published

**JSON-driven, component-based onboarding flows for React Native apps.**

Readme

@codyzk/onboarding

JSON-driven, component-based onboarding flows for React Native apps.

Build flexible, beautiful onboarding experiences with:

  • 🎨 Token-based design system - Centralized colors, spacing, typography
  • 📦 Component array system - Arrange UI elements in any order
  • 🎯 Type-safe - Full TypeScript + Zod validation
  • 🔌 Pluggable - Custom adapters for permissions and ratings
  • 📱 Cross-platform - Works on iOS, Android, and Web

Installation

npm install @codyzk/onboarding

# or
pnpm add @codyzk/onboarding

Quick Start

import { Onboarding } from "@codyzk/onboarding";
import flow from "./flows/onboarding.json";

function App() {
  return (
    <Onboarding
      flow={flow}
      theme="auto" // "light", "dark", or "auto" (follows system)
      onEnd={(result) => {
        // Navigate based on result.dest: "paywall", "home", or "custom"
        if (result.dest === "paywall") {
          navigation.navigate("Paywall");
        } else {
          navigation.navigate("Home");
        }
      }}
      analytics={{
        screen: (node) => {
          analytics.track("Onboarding Screen Viewed", {
            nodeId: node.id,
            type: node.type,
          });
        },
      }}
    />
  );
}

Component Array System v1

The new component system provides complete flexibility over your onboarding UI:

{
  "id": "welcome_flow",
  "version": 1,
  "start": "welcome",
  "nodes": {
    "welcome": {
      "type": "screen",
      "id": "welcome",
      "schemaVersion": 1,
      "tokens": {
        "colors": {
          "brand": "#4F46E5",
          "text": "#111827"
        },
        "spacing": {
          "md": 16,
          "lg": 24
        },
        "typography": {
          "title": { "size": 28, "weight": 700 }
        }
      },
      "components": [
        {
          "id": "root",
          "type": "box",
          "style": { "padding": "lg", "gap": "md" },
          "children": [
            {
              "id": "title",
              "type": "text",
              "content": "Welcome!",
              "typography": { "preset": "title" },
              "style": { "color": "brand" }
            },
            {
              "id": "cta",
              "type": "button",
              "label": "Get Started",
              "action": { "kind": "next", "to": "next_screen" },
              "style": { "backgroundColor": "brand" }
            }
          ]
        }
      ]
    }
  }
}

Key Features:

  • Tokens - Define colors, spacing, radii, typography once
  • Components - Text, image, button, box, spacer, divider
  • Flexible layouts - Nested boxes with flexbox styling
  • Actions - Button actions for navigation, links, events

📖 Read the Implementation Guide for detailed documentation.

API Reference

<Onboarding /> Props

| Prop | Type | Required | Default | Description | | ------------------- | ------------------------------------------------------ | -------------------- | ----------------- | -------------------------------------------------------------------------------------------- | | flow | Flow | Record<string, unknown> | One of flow/flowPath | - | Inline flow object (e.g., import flow from "./flows/default.json") | | flowPath | string | One of flow/flowPath | - | Path to a flow JSON file. Useful in Node/web builds where dynamic require is available. | | onEnd | (result) => void | No | - | Callback when flow ends. Receives { dest: "home" \| "paywall" \| "custom", payload?: any } | | theme | "light" \| "dark" \| "auto" | No | "auto" | Theme mode. "auto" follows system appearance | | permissionAdapter | PermissionAdapter | No | Stub | Custom permission adapter (default uses in-memory stub) | | ratingAdapter | RatingAdapter | No | Noop | Custom rating adapter (default is no-op) | | linkOpener | { open: (url) => Promise<void> } | No | Linking.openURL | Custom link/deep link opener | | analytics | { screen?: (node) => void, end?: (payload) => void } | No | - | Analytics callbacks for tracking |

Example with All Props

import { Onboarding } from "@codyzk/onboarding
";
import flow from "./flows/onboarding.json";

function App() {
  return (
    <Onboarding
      flow={flow}
      theme="auto"
      onEnd={(result) => {
        console.log("Onboarding ended:", result);
        if (result.dest === "paywall") {
          navigation.navigate("Paywall", result.payload);
        } else {
          navigation.navigate("Home");
        }
      }}
      permissionAdapter={customPermissionAdapter}
      ratingAdapter={customRatingAdapter}
      linkOpener={{
        open: async (url) => {
          // Custom deep link handling
          await Linking.openURL(url);
        },
      }}
      analytics={{
        screen: (node) => {
          analytics.track("Screen Viewed", {
            screenId: node.id,
            screenType: node.type,
          });
        },
        end: (payload) => {
          analytics.track("Onboarding Completed", payload);
        },
      }}
    />
  );
}

Theming

The SDK supports light, dark, and auto theming that matches your host app.

Theme Modes

// Auto mode - follows system appearance (default)
<Onboarding theme="auto" ... />

// Force light mode
<Onboarding theme="light" ... />

// Force dark mode
<Onboarding theme="dark" ... />

Per-Flow Theme Override

Set a default theme in your flow JSON:

{
  "id": "onboarding_v1",
  "version": 1,
  "start": "welcome",
  "theme": "dark",
  "nodes": { ... }
}

Priority: Component prop > Flow JSON > "auto" (system default)

Using Theme in Custom Components

import { useTheme } from "@codyzk/onboarding
";

function CustomComponent() {
  const theme = useTheme();

  return (
    <View style={{ backgroundColor: theme.color.surface }}>
      <Text style={{ color: theme.color.text }}>Custom Content</Text>
    </View>
  );
}

Permissions

The SDK includes a pluggable permission adapter system that works without native dependencies for MVP testing.

Default Behavior (Stub Adapter)

By default, the SDK uses an in-memory stub that returns deterministic results:

import {
  StubPermissionAdapter,
  setPermissionAdapter,
} from "@codyzk/onboarding
";

// Configure stub with default outcomes
const adapter = new StubPermissionAdapter({
  camera: "denied",
  location: "granted",
  photoLibrary: "denied",
});

setPermissionAdapter(adapter);

// Later in dev/testing, change outcomes without rebuild
adapter.set("camera", "granted");

Permission Types

Supported permission types:

  • camera
  • microphone
  • location
  • photoLibrary
  • contacts
  • calendar
  • reminders
  • speech
  • motion
  • bluetooth
  • notification

Permission Status

Each permission can have one of three states:

  • granted - User has granted permission
  • denied - User has not granted or explicitly denied
  • blocked - User has permanently denied (requires system settings)

Using in Flows

Define permission nodes in your flow JSON with optional customization:

{
  "p1": {
    "type": "permission",
    "id": "p1",
    "permission": "camera",
    "title": "Camera Access Required",
    "rationale": "We use your camera to scan documents and create quick posts.",
    "allowLabel": "Enable Camera",
    "notNowLabel": "Maybe Later",
    "onGranted": "next_screen",
    "onDenied": "skip_camera_features"
  }
}

Optional Fields:

  • title - Screen title (default: "Permission required")
  • rationale - Explanation text (default: "We need access to continue.")
  • allowLabel - Primary button text (default: "Allow")
  • notNowLabel - Secondary button text (default: "Not now")

Custom Adapter (Production)

For production, implement the PermissionAdapter interface:

import { PermissionAdapter, setPermissionAdapter } from "@codyzk/onboarding
";
import {
  check,
  request,
  openSettings,
  PERMISSIONS,
} from "react-native-permissions";

class NativePermissionAdapter implements PermissionAdapter {
  async check(type: PermissionType): Promise<PermissionStatus> {
    const result = await check(PERMISSIONS.IOS[type.toUpperCase()]);
    return this.mapStatus(result);
  }

  async request(type: PermissionType): Promise<PermissionStatus> {
    const result = await request(PERMISSIONS.IOS[type.toUpperCase()]);
    return this.mapStatus(result);
  }

  async openSettings(): Promise<void> {
    await openSettings();
  }

  private mapStatus(status: string): PermissionStatus {
    if (status === "granted") return "granted";
    if (status === "blocked") return "blocked";
    return "denied";
  }
}

// Swap in production adapter at app startup
setPermissionAdapter(new NativePermissionAdapter());

Development Panel

For testing different permission flows, create a dev panel:

import {
  StubPermissionAdapter,
  setPermissionAdapter,
} from "@codyzk/onboarding
";

// Initialize stub
const adapter = new StubPermissionAdapter();
setPermissionAdapter(adapter);

// Dev UI to toggle outcomes
function PermissionDevPanel() {
  return (
    <View>
      <Button onPress={() => adapter.set("camera", "granted")}>
        Grant Camera
      </Button>
      <Button onPress={() => adapter.set("camera", "denied")}>
        Deny Camera
      </Button>
      <Button onPress={() => adapter.set("location", "granted")}>
        Grant Location
      </Button>
    </View>
  );
}

Edge Cases

  • Unknown permission type: Treated as denied (won't crash)
  • Missing onGranted or onDenied: Flow ends to prevent getting stuck
  • Adapter throws: Treated as denied to avoid blocking user
  • blocked status: Routes to onDenied for MVP (later can show settings prompt)

Rating / Review Requests

The SDK includes a pluggable rating adapter for in-app review prompts.

Default Behavior (No-op Adapter)

By default, the SDK uses a no-op adapter that resolves immediately without showing any UI:

import { NoopRatingAdapter, setRatingAdapter } from "@codyzk/onboarding
";

// Default behavior - no-op for MVP
// Nothing happens when review is requested

Using in Flows

Define rate nodes with two trigger modes:

Immediate Mode (auto-trigger on node):

{
  "rate1": {
    "type": "rate",
    "id": "rate1",
    "trigger": "immediate",
    "next": "next_screen"
  }
}

After-Next Mode (show button):

{
  "rate2": {
    "type": "rate",
    "id": "rate2",
    "trigger": "after_next",
    "next": "next_screen"
  }
}

Custom Adapter (Production)

For production, implement the RatingAdapter interface:

import { RatingAdapter, setRatingAdapter } from "@codyzk/onboarding
";
import { requestReview } from "react-native-store-review";

class NativeRatingAdapter implements RatingAdapter {
  async requestReview(): Promise<void> {
    try {
      // iOS: StoreReviewController.requestReview()
      // Android: Play In-App Review API
      await requestReview();
    } catch (error) {
      // Swallow errors - never block flow
      console.error("Review request failed:", error);
    }
  }
}

// Set at app startup
setRatingAdapter(new NativeRatingAdapter());

Timeout Protection

The rate node automatically times out after 2.5 seconds to prevent blocking:

  • If the adapter takes too long, flow continues anyway
  • User is never stuck waiting
  • Ensures smooth progression

Edge Cases

  • No adapter provided: Falls back to NoopRatingAdapter (no-op)
  • Adapter throws: Error caught, flow continues to next
  • Timeout: After 2.5s, flow continues regardless
  • Multiple calls: Safe to call multiple times (idempotency handled by adapter)

Flow JSON Schema

Screen Nodes (Component System v1)

Screen nodes use the new component array system with tokens and flexible layouts:

{
  type: "screen",
  id: string,
  schemaVersion: 1,
  tokens: {
    colors: Record<string, string>,      // e.g., { "brand": "#4F46E5" }
    spacing: Record<string, number>,     // e.g., { "md": 16 }
    radii: Record<string, number>,       // e.g., { "md": 12 }
    typography: Record<string, {         // e.g., { "title": { size: 28, weight: 700 } }
      size: number,
      lineHeight?: number,
      weight?: number,
      letterSpacing?: number
    }>
  },
  components: Component[],               // Array of UI components
  next?: string,                         // Optional next node reference
  tags?: string[]                        // Optional tags for analytics
}

Component Types

Box (Container)

{
  "type": "box",
  "id": "unique-id",
  "style": {
    "flex": 1,
    "flexDirection": "column",
    "gap": "md",
    "padding": "lg",
    "justifyContent": "center",
    "alignItems": "center"
  },
  "children": [
    /* nested components */
  ]
}

Text

{
  "type": "text",
  "id": "unique-id",
  "content": "Your text here",
  "typography": {
    "preset": "title", // or direct values
    "size": 24,
    "weight": 600
  },
  "style": {
    "color": "text",
    "textAlign": "center"
  }
}

Button

{
  "type": "button",
  "id": "unique-id",
  "label": "Button Text",
  "action": {
    "kind": "next",
    "to": "next-node-id"
  },
  "style": {
    "backgroundColor": "brand",
    "padding": "md",
    "borderRadius": "md",
    "color": "#FFFFFF"
  },
  "a11yLabel": "Accessibility label"
}

Image

{
  "type": "image",
  "id": "unique-id",
  "url": "https://example.com/image.png",
  "aspectRatio": 1.6,
  "resizeMode": "cover",
  "style": {
    "borderRadius": "md"
  }
}

Spacer

{
  "type": "spacer",
  "id": "unique-id",
  "size": "lg" // or numeric: 24
}

Divider

{
  "type": "divider",
  "id": "unique-id",
  "style": {
    "backgroundColor": "surface",
    "height": 1
  }
}

Other Node Types

Permission Node

{
  "type": "permission",
  "id": "unique-id",
  "permission": "camera",
  "title": "Camera Access",
  "rationale": "We need camera access to scan documents",
  "allowLabel": "Enable Camera",
  "notNowLabel": "Maybe Later",
  "onGranted": "next-node-id",
  "onDenied": "skip-node-id"
}

Rate Node

{
  "type": "rate",
  "id": "unique-id",
  "trigger": "immediate", // or "after_next"
  "next": "next-node-id"
}

End Node

{
  "type": "end",
  "id": "unique-id",
  "result": "home", // or "paywall" | "custom"
  "payload": {
    "completed": true,
    "any": "custom data"
  }
}

Examples

See example flows in the flows/ directory:

Documentation

License

MIT