@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-sdkimport 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.Reactandwindow.ReactDOM. In a bundled React app these are not onwindowby default — you must assign them before creating aJournyMessaginginstance. 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. Seedocs/widget-mode.md.'list'(default) — Resizable scrollable panel listing all messages at once. Best for dashboards and message-history surfaces. Seedocs/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 withbannerPositionto anchor it andbannerAutoDismissMsto tune (or disable) the timer. Seedocs/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: grabfor 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.
