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

@journyio/messaging-sdk

v1.0.7

Published

Journy In-App Messaging Library

Readme

@journyio/messaging-sdk

A standalone JavaScript library for displaying in-app messages from Journy. This library can be integrated via a <script> tag, ES modules, or npm package, similar to analytics libraries like Segment or Google Analytics.

Features

  • 🚀 Standalone Library: Works without a build step in the host application
  • ⚛️ React Support: Automatically renders React components when React is available
  • 🔒 XSS Protection: Built-in HTML sanitization using DOMPurify
  • 📦 Lightweight: Minimal dependencies, small bundle size
  • 🎨 Customizable: Flexible styling and message types
  • 📊 Event Tracking: Automatic tracking of message interactions
  • 🔄 Message Queue: Smart queue management with priority support
  • ⏱️ Auto-polling: Automatically fetches new messages

Installation

Method 1: Script Tag (Simplest)

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <!-- Your app content -->

  <!-- Load React and ReactDOM (if not already loaded) -->
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

  <!-- Load Journy Messages Library -->
  <script src="https://cdn.journy.io/messages/journy-messages.min.js"></script>

  <script>
    // Initialize
    const messaging = new JournyMessages({
      writeKey: 'your-write-key',
      userId: 'user-123',
      entityType: 'user',
    });
  </script>
</body>
</html>

Method 2: npm Package

npm install @journyio/messaging-sdk
import React from 'react';
import ReactDOM from 'react-dom';
import { JournyMessaging } from '@journyio/messaging-sdk';

// Make React available globally for the SDK's internal rendering
window.React = React;
window.ReactDOM = ReactDOM;

const messaging = new JournyMessaging({
  writeKey: 'your-write-key',
  userId: 'user-123',
  entityType: 'user',
});

Method 3: ES Modules

import JournyMessages from '@journyio/messaging-sdk';

const messaging = new JournyMessages({
  writeKey: 'your-write-key',
  userId: 'user-123',
  entityType: 'user',
});

Note: The SDK renders its widget using window.React and window.ReactDOM. In a bundled React app these are not on window by default — you must assign them before creating a JournyMessaging instance. When using script tags, the UMD builds set these globals automatically.

React Integration

Using a Component

Create a reusable component that initializes and cleans up the SDK:

// src/components/JournyWidget.tsx
import { useEffect, useRef } from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import { JournyMessaging } from '@journyio/messaging-sdk';

window.React = React;
window.ReactDOM = ReactDOM;

interface Props {
  writeKey: string;
  userId?: string;
  accountId?: string;
  entityType: 'user' | 'account';
}

export function JournyWidget({ writeKey, userId, accountId, entityType }: Props) {
  const messagingRef = useRef<JournyMessaging | null>(null);

  useEffect(() => {
    messagingRef.current = new JournyMessaging({
      writeKey,
      entityType,
      userId,
      accountId,
    });

    return () => {
      messagingRef.current?.destroy();
      messagingRef.current = null;
    };
  }, [writeKey, entityType, userId, accountId]);

  return null; // The SDK manages its own DOM
}

Then use it anywhere in your app:

import { JournyWidget } from './components/JournyWidget';

function App() {
  return (
    <div>
      <h1>My Application</h1>
      <JournyWidget
        writeKey="your-write-key"
        entityType="user"
        userId="user-123"
      />
    </div>
  );
}

Using a Custom Hook (after authentication)

A common pattern is to start the widget only after the user logs in:

// src/hooks/useJournyMessaging.ts
import { useEffect, useRef } from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import { JournyMessaging } from '@journyio/messaging-sdk';

window.React = React;
window.ReactDOM = ReactDOM;

export function useJournyMessaging(user: { id: string } | null) {
  const messagingRef = useRef<JournyMessaging | null>(null);

  useEffect(() => {
    if (!user) return;

    messagingRef.current = new JournyMessaging({
      writeKey: 'your-write-key',
      entityType: 'user',
      userId: user.id,
    });

    return () => {
      messagingRef.current?.destroy();
      messagingRef.current = null;
    };
  }, [user?.id]);

  return messagingRef;
}
// src/App.tsx
import { useJournyMessaging } from './hooks/useJournyMessaging';
import { useAuth } from './auth';

function App() {
  const { user } = useAuth();
  useJournyMessaging(user);

  return <div>{/* your app */}</div>;
}

Configuration

Basic Configuration

const messaging = new JournyMessaging({
  writeKey: 'your-write-key',        // Required: Your Journy write key
  userId: 'user-123',                 // Optional: User ID
  accountId: 'account-456',           // Optional: Account ID
  entityType: 'user',                 // Required: 'user' or 'account'
  apiEndpoint: 'https://analyze.journy.io', // Optional: API base URL
  pollingInterval: 30000,            // Optional: Polling interval in ms (default: 30000)
  isCollapsed: false,                // Optional: Start collapsed or expanded
  renderTarget: 'self',             // Optional: 'self', 'parent', or 'top'
  hideUntilMessages: true,          // Optional: Hide the widget until first message is fetched (default: true)
});

Configuration Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | writeKey | string | Yes | - | Your Journy write key for authentication | | userId | string | No | - | Current user ID | | accountId | string | No | - | Current account ID | | entityType | 'user' \| 'account' | Yes | - | Type of entity to fetch messages for | | apiEndpoint | string | No | 'https://analyze.journy.io' | API base URL | | pollingInterval | number | No | 30000 | Interval in milliseconds to poll for new messages | | isCollapsed | boolean | No | false | Whether the widget starts collapsed | | styles | 'default' \| 'none' \| { url: string } \| { css: string } | No | 'default' | Style injection mode | | renderTarget | 'self' \| 'parent' \| 'top' | No | 'self' | Which document to render the widget in (useful for iframes) | | hideUntilMessages | boolean | No | true | Keep the widget hidden until the first message has been fetched. Set to false to mount the widget shell immediately. | | displayMode | 'widget' \| 'list' \| 'banner' | No | 'list' | How messages are surfaced. See Display Modes below. | | bannerPosition | 'top-left' \| 'top-center' \| 'top-right' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right' | No | 'top-center' | Anchor for displayMode: 'banner'. Ignored in other modes. | | bannerAutoDismissMs | number | No | 20000 | Auto-dismiss delay (ms) for banner mode. Set to 0 to keep the banner on screen until the user dismisses it manually. Ignored in other modes. |

Display Modes

The SDK can surface messages in three different ways via displayMode:

  • 'widget' — Compact, draggable single-message widget anchored to the viewport. The user pages through messages with prev/next arrows. See docs/widget-mode.md.
  • 'list' (default) — Resizable scrollable panel listing all messages at once. Best for dashboards and message-history surfaces. See docs/list-mode.md.
  • 'banner' — Slim strip pinned to one of six viewport edges, showing the current message and auto-dismissing after a configurable delay (default 20 seconds) without user interaction. Hovering pauses the timer. Pair with bannerPosition to anchor it and bannerAutoDismissMs to tune (or disable) the timer. See docs/banner-mode.md.
// Banner pinned to the top-right that disappears after 30s
const messaging = new JournyMessages({
  writeKey: 'your-write-key',
  entityType: 'user',
  displayMode: 'banner',
  bannerPosition: 'top-right',
  bannerAutoDismissMs: 30000,
});

// Sticky banner — never auto-dismisses
const sticky = new JournyMessages({
  writeKey: 'your-write-key',
  entityType: 'user',
  displayMode: 'banner',
  bannerAutoDismissMs: 0,
});

End users can also switch modes, pick a banner position, and choose the auto-dismiss timing at runtime via the in-app debug Settings panel, when window.__JOURNY_DEBUG__ = { settings: true } is enabled before the SDK loads.

API Reference

Methods

markAsRead(messageIds: string | string[]): Promise<void>

Marks one or more messages as read and removes them from the queue.

await messaging.markAsRead('message-123');
await messaging.markAsRead(['message-123', 'message-456']);

trackLinkClick(messageId: string, linkUrl: string): void

Tracks when a user clicks a link in a message.

messaging.trackLinkClick('message-123', 'https://example.com');

trackMessageClosed(messageId: string): void

Tracks when a user closes a message.

messaging.trackMessageClosed('message-123');

trackMessageOpened(messageId: string): void

Tracks when a message is opened/displayed.

messaging.trackMessageOpened('message-123');

destroy(): void

Cleans up the messaging instance, stops polling, and removes UI elements.

messaging.destroy();

Message Format

Messages returned by the API follow this shape:

type MessageStatus = 'pending' | 'sent' | 'read' | 'expired';
type MessageScope = 'account' | 'user';

interface Message {
  id: string;
  appId: string;
  accountId?: string;
  userId?: string;
  status: MessageStatus;
  scope: MessageScope;
  message: string;             // HTML content (will be sanitized)
  received: boolean;
  expired: boolean;
  createdAt: string;           // ISO 8601 timestamp
  expiredAt?: string;          // ISO 8601 timestamp
}

Styling

Default styles

When you do not pass a styles option (or set styles: 'default'), the SDK injects the default styles automatically. You do not need to add a <link> tag; a single script tag is enough for the widget to look correct.

If you prefer to load the CSS yourself (e.g. for caching), you can still link the built file and set styles: 'none' then include journy-messages.css in your page—but the typical use is to omit styles and let the SDK inject the default CSS.

Configurable styles

You can control styling via the styles config option:

  • styles: 'default' or omitted – The SDK injects the default styles inline. No separate CSS file needed.
  • styles: 'none' – No SDK styles are injected. You provide all CSS (e.g. target .journy-message-widget, .journy-message-popup, etc.) in your own stylesheet.
  • styles: { url: 'https://...' } – The SDK injects the default styles, then a <link rel="stylesheet" href="..."> pointing to your stylesheet on top of them, so your stylesheet overrides only what it declares.
  • styles: { css: '.journy-message-widget { ... }' } – The SDK injects the default styles, then a <style> tag with the given CSS on top of them. Your CSS layers over the defaults (later rules win the cascade) instead of replacing them.

Example with custom stylesheet URL:

const messaging = new JournyMessages({
  writeKey: 'your-write-key',
  entityType: 'user',
  styles: { url: 'https://my-app.com/journy-messages-theme.css' },
});

Example with no library styles (you style everything):

const messaging = new JournyMessages({
  writeKey: 'your-write-key',
  entityType: 'user',
  styles: 'none',
});

See examples/alternative-styles.html and examples/alternative-styles.css for a test theme and styles: { url: '...' } usage.

Overriding default styles

When using default styles, you can still customize by overriding these CSS classes in your own CSS:

  • .journy-message-overlay - The backdrop overlay
  • .journy-message-popup - The message popup container
  • .journy-message-title - Message title
  • .journy-message-content - Message content area
  • .journy-message-close - Close button
  • .journy-message-actions - Action buttons container
  • .journy-message-action - Individual action button

Banner mode classes (displayMode: 'banner')

Banner mode renders outside the regular widget shell and exposes its own class set:

  • .journy-message-banner - The banner container (navy panel, position: fixed, cursor: grab for the drag-to-reposition interaction)
  • .journy-message-banner-pin - The pin indicator (📍/📌) in the top-left corner; non-interactive
  • .journy-message-banner-content - Scrollable wrapper around the sanitized message HTML and timestamp (overflow-y: auto, max-height: calc(50vh - 52px))
  • .journy-message-banner-close - The dismiss (×) button in the top-right corner
  • .journy-message-banner-exiting - Applied during the 200 ms fade-out transition before the banner unmounts
  • .journy-message-banner-pinned - Applied while the banner is pinned open (click-to-toggle); draws a 2 px blue outline ring and suspends the auto-dismiss timer

Position modifiers — one of these is applied alongside .journy-message-banner based on bannerPosition (dropped once the user drags the banner, at which point absolute left/top are written via inline styles):

  • .journy-message-banner-top-left
  • .journy-message-banner-top-center
  • .journy-message-banner-top-right
  • .journy-message-banner-bottom-left
  • .journy-message-banner-bottom-center
  • .journy-message-banner-bottom-right

Animation@keyframes journy-banner-fade-in (opacity only) is shared by all six positions so the static translateX(-50%) on centered variants is preserved. Override or disable via the animation property on .journy-message-banner.

Scrollbar.journy-message-banner-content ships a thin dark-surface scrollbar (tinted with --journy-on-header-btn-hover/--journy-on-header-btn on a transparent track) so long-form HTML messages stay legible on the navy background. The light-surface counterpart on .journy-message-widget-content in list/widget modes uses the same shape recoloured for white backgrounds (--journy-border-strong / --journy-text-muted). Override either via ::-webkit-scrollbar-thumb and scrollbar-color.

Sizing — the banner is shrink-to-fit up to max-width: 80vw with box-sizing: border-box, so the rendered width (including padding) is capped at 80 % of the viewport at every breakpoint. Override that ceiling on .journy-message-banner if you want a different cap.

Example — re-skin the banner to a brand red:

.journy-message-banner {
  background: #b91c1c;
  box-shadow: 0 6px 28px rgba(0, 0, 0, 0.3);
}

.journy-message-banner .journy-message-content {
  color: #fff;
  font-weight: 500;
}

.journy-message-banner-close {
  color: rgba(255, 255, 255, 0.85);
}

.journy-message-banner-content::-webkit-scrollbar-thumb {
  background-color: rgba(255, 255, 255, 0.35);
}

Message Type Classes

  • .journy-message-info - Info messages (blue border)
  • .journy-message-success - Success messages (green border)
  • .journy-message-warning - Warning messages (orange border)
  • .journy-message-error - Error messages (red border)

Rendering Inside an Iframe

If the SDK is loaded inside an iframe and you want the widget to appear on the parent page:

const messaging = new JournyMessages({
  writeKey: 'your-write-key',
  entityType: 'user',
  userId: 'user-123',
  renderTarget: 'parent', // or 'top' for the top-level window
});

The parent page must be same-origin. If cross-origin, the SDK falls back to rendering inside the iframe.

Security

XSS Prevention

The library uses DOMPurify to sanitize all HTML content before rendering. Only the following HTML tags and attributes are allowed:

Allowed Tags:

  • a, b, i, em, strong, p, br, ul, ol, li

Allowed Attributes:

  • href, target, rel (for links)

All other HTML is stripped to prevent XSS attacks.

Browser Support

  • Chrome (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

License

MIT

Support

For issues and questions, please visit GitHub Issues.