
Security News
/Research
Wallet-Draining npm Package Impersonates Nodemailer to Hijack Crypto Transactions
Malicious npm package impersonates Nodemailer and drains wallets by hijacking crypto transactions across multiple blockchains.
@sammy-labs/walkthroughs
Advanced tools
This package provides a simple, Provider + Hook architecture to integrate Sammy's interactive walkthroughs into your React (or Next.js) application. It manages global state, highlights elements on the page, and guides the user step-by-step through a sequence of interactions.
Key features include:
WalkthroughProvider
to manage authentication tokens, driver instances, and global config.useWalkthrough
to start, stop, or control walkthroughs anywhere in your app.Install the package via npm or yarn:
npm install @sammy-labs/walkthroughs
# or
yarn add @sammy-labs/walkthroughs
This will give you access to:
WalkthroughProvider
(the React provider)useWalkthrough
(the React hook)FlowError
, WalkthroughResponse
, etc.)Ensure you also import the CSS file when needed:
@import "@sammy-labs/walkthroughs/dist/index.css";
Or in a Next.js / Plasmo environment:
import "@sammy-labs/walkthroughs/dist/index.css";
<WalkthroughProvider>
useWalkthrough
Hook
startWithId(flowId)
to fetch from the Sammy API by flow ID.startWithData(yourFlowObject)
to start from pre-fetched or user-provided data.stop()
to terminate any running walkthrough.isActive()
).Driver & Global Configuration
Walkthrough Data
WalkthroughResponse
) which it will parse into a series of steps. Each step references an element (or set of elements) to highlight and the text to display to the user.Below is a minimal usage example:
Wrap your application:
import React from "react";
import { WalkthroughProvider } from "@sammy-labs/walkthroughs";
import "@sammy-labs/walkthroughs/dist/index.css";
function MyApp({ Component, pageProps }) {
const sammyToken = "YOUR_JWT_TOKEN"; // typically fetched from your server
return (
<WalkthroughProvider
token={sammyToken}
baseUrl="https://api.sammylabs.com"
driverConfig={{ overlayOpacity: 0.7 }}
config={{ debug: true }}
>
<Component {...pageProps} />
</WalkthroughProvider>
);
}
export default MyApp;
Use the Hook in any component:
import React from "react";
import { useWalkthrough } from "@sammy-labs/walkthroughs";
export function StartButton() {
const { startWithId, isActive, stop } = useWalkthrough();
const handleStart = async () => {
// Start a walkthrough by ID from the Sammy hosted API
const success = await startWithId("12345");
if (!success) {
alert("Failed to start Sammy walkthrough.");
}
};
return (
<div>
<button onClick={handleStart}>Start Walkthrough #12345</button>
{isActive() && <button onClick={stop}>Stop Walkthrough</button>}
</div>
);
}
That's enough to get you started! The library will automatically fetch data for that flow ID, highlight elements, show popovers, and proceed step by step.
Below is a typical Next.js integration, referencing your environment variables to generate the Sammy token.
app/providers/SammyWalkthroughProvider.tsx
):"use client";
import React, { useState, useEffect } from "react";
import { WalkthroughProvider } from "@sammy-labs/walkthroughs";
export default function SammyWalkthroughProvider({ children }) {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
// Example: fetch or generate a token from your server
async function fetchToken() {
const resp = await fetch("/api/sammy-auth");
const data = await resp.json();
setToken(data.token);
}
fetchToken();
}, []);
if (!token) {
return <div>Loading walkthrough token...</div>;
}
return (
<WalkthroughProvider
token={token}
baseUrl={process.env.NEXT_PUBLIC_SAMMY_BASE_URL}
onTokenExpired={() => {
console.log("Sammy token expired, re-fetching");
}}
onError={(err) => console.warn("Walkthrough error:", err)}
driverConfig={{ overlayOpacity: 0.6 }}
config={{ debug: false, askInput: true }}
>
{children}
</WalkthroughProvider>
);
}
// app/layout.tsx or pages/_app.tsx
import SammyWalkthroughProvider from "./providers/SammyWalkthroughProvider";
export default function RootLayout({ children }) {
return (
<html>
<body>
<SammyWalkthroughProvider>{children}</SammyWalkthroughProvider>
</body>
</html>
);
}
"use client";
import React from "react";
import { useWalkthrough } from "@sammy-labs/walkthroughs";
export default function Dashboard() {
const { startWithId, stop, isActive } = useWalkthrough();
return (
<div>
<button onClick={() => startWithId("8750")}>Show Onboarding Tips</button>
{isActive() && <button onClick={stop}>Stop</button>}
</div>
);
}
WalkthroughProvider
A React provider that must wrap any area that uses the library.
Props
Prop | Type | Default | Description |
---|---|---|---|
token | string | null | null | A short-lived JWT token used to fetch data from the Sammy API. If omitted, only local flows (startWithData() ) will work. |
baseUrl | string | "http://localhost:8000" | The base URL for Sammy's endpoints. |
onTokenExpired | () => void | none | Called when a 401 occurs, letting you re-fetch a new token. |
onError | (error: FlowError) => void | none | Called when any error occurs (element not found, API failure, etc.). |
driverConfig | Partial<DriverConfig> | { ... } | Fine-grained driver settings (overlay color, popover offsets, etc.) |
config | Partial<WalkthroughConfig> | { ... } | Global config options (debug, domain override, fallback settings, etc.). |
logoUrl | string | "" | An optional logo to display in the popovers. |
children | ReactNode | required | Your app or subtree. |
Example:
<WalkthroughProvider
token="..."
baseUrl="https://api.sammylabs.com"
onTokenExpired={() => console.log("Token expired, re-auth!")}
onError={(e) => console.error("Walkthrough error:", e)}
driverConfig={{ overlayOpacity: 0.7 }}
config={{ debug: true, askInput: false }}
>
<App />
</WalkthroughProvider>
useWalkthrough(options?)
Hook that returns convenience methods and the current walkthrough state.
Signature:
function useWalkthrough(options?: {
checkQueryOnMount?: boolean;
onError?: (error: FlowError) => void;
waitTime?: number;
driverConfig?: Partial<DriverConfig>;
config?: Partial<WalkthroughConfig>;
disableRedirects?: boolean;
autoStartPendingWalkthrough?: boolean;
fallbackTimeout?: number;
}): {
startWithId(flowId: string | number): Promise<boolean>;
startWithData(
data: WalkthroughResponse | WalkthroughSearchResult | any
): Promise<boolean>;
stop(): boolean;
isActive(): boolean;
configure(driverConfig: Partial<DriverConfig>): void;
configureGlobal(globalConfig: Partial<WalkthroughConfig>): void;
state: {
isTokenValid: boolean;
isLoading: boolean;
error: FlowError | null;
token: string | null;
baseUrl: string;
isActive: boolean;
config: WalkthroughConfig;
};
};
checkQueryOnMount?: boolean
If true
, automatically checks the URL query parameters (default key sammy_flow_id
) on mount to see if a walkthrough should be started.
onError?: (error: FlowError) => void
Local error handler specifically for this hook usage. Merged with or overrides the provider-level onError
.
waitTime?: number
Milliseconds to wait before automatically starting a walkthrough from query parameters.
driverConfig?: Partial<DriverConfig>
Additional driver config merges on top of the provider config.
config?: Partial<WalkthroughConfig>
Additional global config merges on top of the provider config.
disableRedirects?: boolean
If true
, will not auto-redirect the user if the recorded URL in the walkthrough data doesn't match the current page.
autoStartPendingWalkthrough?: boolean
If true
, automatically attempts to start any pending walkthrough data that was stored from a previous page (useful in multi-page flows).
fallbackTimeout?: number
Maximum time in milliseconds to wait for DOM elements to be found before using fallback elements. Defaults to 10000 (10s). This is deliberately separate from the main config object to avoid React re-render issues when used within effects.
startWithId(flowId)
Fetches a walkthrough from the Sammy API using flowId
and starts it.
Returns true
if successful, false
if not.
startWithData(data)
Starts a walkthrough using pre-fetched or user-provided data.
WalkthroughResponse
, or older shapes like search results.true
if success, false
otherwise.stop()
Stops any active walkthrough, removing highlights/overlays.
isActive()
Returns true
if a walkthrough is currently in progress.
configure(driverConfig)
Dynamically update the driver config.
configureGlobal(globalConfig)
Dynamically update the global config.
isTokenValid: boolean
isLoading: boolean
error: FlowError \| null
token: string \| null
baseUrl: string
isActive: boolean
isActive()
.config: WalkthroughConfig
WalkthroughConfig
)export type WalkthroughConfig = {
// Query parameter key for auto-starting flow, default "sammy_flow_id"
flowIdQueryParam: string;
// Wait time before automatically starting a flow from query param
waitTimeAfterLoadMs: number;
// Maximum number of attempts to find a DOM element
maxElementFindAttempts: number;
// Timeout for each attempt to find an element, in ms
elementFindTimeoutMs: number;
// How long the DOM must remain stable before searching for elements
domStabilityMs: number;
// Max total time to wait for DOM to be stable
maxDomStabilityWaitMs: number;
// Enables debug logging
debug: boolean;
// Default base URL for the Sammy API
apiBaseUrl: string;
// Optional logo URL for popovers
logoUrl: string;
// Base URL for screenshot images
imageBaseUrl: string;
// Domain override used to rewrite step URLs if different from environment
overrideDomainUrl: string;
// Whether to show an "Ask a question" input in the popover
askInput: boolean;
// Whether to log events to Sammy
enableLogging: boolean;
};
Common fields:
debug
: set to true
to see console logs about fallback elements, element searches, etc.overrideDomainUrl
: if the recorded steps have different domain references than your current domain, you can override them.Example:
<WalkthroughProvider
config={{
debug: true,
maxElementFindAttempts: 3,
elementFindTimeoutMs: 10000,
overrideDomainUrl: "https://demo.deel.com",
}}
>
<App />
</WalkthroughProvider>
Example with fallbackTimeout:
// Using the hook with fallbackTimeout
function WalkthroughButton() {
const { startWithId } = useWalkthrough({
// Separate from config to avoid re-render issues in effects
fallbackTimeout: 15000, // Wait up to 15 seconds for elements to be found
config: {
debug: true,
overrideDomainUrl: "https://demo.deel.com",
},
});
return (
<button onClick={() => startWithId("12345")}>Start Walkthrough</button>
);
}
DriverConfig
)These are more about the highlight overlay and popover.
export interface DriverConfig {
steps?: DriveStep[];
enableLogging?: boolean;
animate?: boolean;
overlayColor?: string; // default #000
overlayOpacity?: number; // default 0.7
smoothScroll?: boolean; // default false
allowClose?: boolean; // default true
overlayClickBehavior?: "close" | "nextStep";
stagePadding?: number; // default 8
stageRadius?: number; // default 12
disableActiveInteraction?: boolean; // default false
allowKeyboardControl?: boolean; // default true
popoverClass?: string;
popoverOffset?: number;
showButtons?: ("next" | "previous" | "close")[];
disableButtons?: ("next" | "previous" | "close")[];
showProgress?: boolean;
progressText?: string; // e.g. "{{current}} of {{total}}"
nextBtnText?: string;
prevBtnText?: string;
doneBtnText?: string;
logoUrl?: string;
// Lifecycle callbacks
onHighlightStarted?: DriverHook;
onHighlighted?: DriverHook;
onDeselected?: DriverHook;
onDestroyStarted?: DriverHook;
onDestroyed?: DriverHook;
onNextClick?: DriverHook;
onPrevClick?: DriverHook;
onCloseClick?: DriverHook;
}
Example:
// Overriding certain fields:
<WalkthroughProvider
driverConfig={{
overlayOpacity: 0.5,
animate: true,
showButtons: ["next", "previous"],
doneBtnText: "Done!",
}}
>
<App />
</WalkthroughProvider>
While normally you only need the Hook, there are some lower-level helpers if you want direct control:
executeApiFlow(flowId, orgId, token, baseUrl, onError)
executeFlowWithData(data, onError, options)
WalkthroughResponse
(or older format) and starts it.These are used internally by startWithId
and startWithData
.
Your token typically expires after a certain time. If the user is still going through a walkthrough and the token expires, the library calls onTokenExpired()
, giving you a chance to refresh it. Then you can pass the new token back to the provider (e.g., via React state).
<WalkthroughProvider
token={myToken}
onTokenExpired={() => {
// e.g., re-fetch new token and re-set the parent state
}}
>
...
</WalkthroughProvider>
If you already have the data for a flow (e.g., from server side rendering or some custom server endpoint), you can pass it directly to startWithData()
:
const { startWithData } = useWalkthrough();
// Suppose you have a big object "myWalkthrough" that matches WalkthroughResponse
await startWithData(myWalkthrough);
No additional fetch calls are needed in that scenario.
Currently, the library supports one active walkthrough at a time. If you try to start a new one while one is running, it will close the previous driver. If you want to queue multiple flows, you can do so sequentially:
const { startWithId, stop } = useWalkthrough();
// Example queue approach
await startWithId("flowA");
// After finishing or stopping, then
await startWithId("flowB");
The walkthrough package includes a built‐in logging mechanism that automatically sends events such as start, step, finish, redirect, and abandon to your Sammy API endpoint. These events are defined in the LogEventType
enum and encapsulate key information about the walkthrough's progress.
The default logging mechanism (to the Sammy hosted API) is handled by the logWalkthroughEvent()
function, located in packages/walkthroughs/src/lib/log.ts
. It performs two main tasks:
onWalkthroughEvent
) — letting you intercept logs locally.baseUrl
) to record that event.You can customize logging in two major ways:
onWalkthroughEvent
callback in your <WalkthroughProvider>
.logWalkthroughEvent()
to change how the package logs to your server.onWalkthroughEvent
in <WalkthroughProvider>
When you set up the Walkthrough Provider, you can attach a callback named onWalkthroughEvent
that fires immediately whenever the walkthrough emits an event. Example:
import React from "react";
import {
WalkthroughProvider,
type LogEvent,
LogEventType,
} from "@sammy-labs/walkthroughs";
function App() {
return (
<WalkthroughProvider
token="YOUR_JWT_TOKEN"
baseUrl="https://api.sammylabs.com"
onWalkthroughEvent={(event: LogEvent) => {
// Log it:
console.log("WalkthroughEvent Received:", event.event_type);
// Example switch on event type:
switch (event.event_type) {
case LogEventType.START:
// The user started the walkthrough
break;
case LogEventType.STEP:
// The user advanced to a new step
break;
case LogEventType.FINISH:
// The user completed the entire walkthrough
break;
// etc.
default:
break;
}
// Forward to your own analytics if you like
myAnalyticsService.track("sammy_walkthrough_event", event);
}}
>
{/* ...your app... */}
</WalkthroughProvider>
);
}
Common Use Cases:
logWalkthroughEvent()
Under the hood, the library calls a helper function logWalkthroughEvent(event, token, baseUrl)
to record each event server‐side. It is located in packages/walkthroughs/src/lib/log.ts
.
export async function logWalkthroughEvent(
event: LogEvent,
token: string,
baseUrl: string
): Promise<LogResponse | void> {
// 1) If there's an `onWalkthroughEvent` callback in WalkthroughProvider, call it:
const globalContext = (window as any)?.__WALKTHROUGH_CONTEXT__;
if (globalContext && typeof globalContext.onWalkthroughEvent === "function") {
globalContext.onWalkthroughEvent(event);
}
// 2) Then send the event to the Sammy hosted API:
try {
const response = await fetch(`${baseUrl}/public/walkthrough/log`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ event }),
});
if (!response.ok) {
throw new Error(`Failed to log event: ${response.statusText}`);
}
// The response typically returns { event_type, timestamp }
return await response.json();
} catch (err) {
console.error("logWalkthroughEvent error:", err);
}
}
Each event object includes fields to identify the status of the walkthrough. For reference, LogEvent
is a union type that may have:
event_type
— One of: start | step | finish | abandon | redirect | fallback
.user_replay_id
— A client-generated ID to group all events for one walkthrough session.user_replay_step_id
— A unique ID for each step event within a session.step_number
— (Optional) 1‐based step index (if relevant).flow_id
, flow_version_id
— Identifiers used in START
events.status
— A short code describing the step's status, e.g. "clicked"
, "finished"
, "abandoned"
.Using the onWalkthroughEvent
callback is generally simplest. For example, to POST each event to your own server:
<WalkthroughProvider
token={token}
baseUrl="https://api.sammylabs.com"
onWalkthroughEvent={(event) => {
// 1) Immediately forward to your analytics
fetch("https://myserver.com/internal-analytics", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event }),
});
}}
>
{/* app code */}
</WalkthroughProvider>
This ensures:
What are they?
step_type?: "informational"
in both HistoryStep
and DriveStep
.Key Points
step_type: "informational"
in your API response or in a manual step definition to trigger corner popovers..sammy-labs-informational-step
) to apply special styling.video_info
object to show a YouTube embed.Example:
{
"step_num": 3,
"step_type": "informational",
"video_info": {
"youtube_url": "https://www.youtube.com/watch?v=abc123&t=60"
},
"state": {
"url": "https://example.com/dashboard",
"interacted_element": []
},
"result": [],
"model_output": {
/* ... */
}
}
What are they?
MutationObserver
on the observer_element
to wait until the real interactiveElements
become visible or inserted.Usage
observer_element?: InteractiveElement
in your step.observer_element
first, and then watch for the real interactive element to appear.Example:
{
"step_num": 2,
"observer_element": {
"xpath": "//div[@class='dynamic-menu']",
"attributes": { "class": "dynamic-menu" }
},
"interactiveElements": [
{
"xpath": "//button[text()='Add to Cart']",
"attributes": { "type": "button" }
}
]
}
Why?
Configuration
In WalkthroughProvider
, set:
<WalkthroughProvider
...
locationChangeEvents={true}
locationChangePollInterval={500} // How often to poll for location changes
locationChangeDebug={false} // Whether to log debug info
...
>
Or set these in the global config
object:
config={{
enableLocationChangeEvents: true,
locationChangePollInterval: 500,
locationChangeDebug: false
}}
Behavior
locationchange
events.url
field).What & How
video_info
with youtube_url
(and optionally start_time
).<iframe>
into the popover.Example:
{
"step_num": 4,
"step_type": "informational",
"video_info": {
"youtube_url": "https://youtu.be/VIDEOID?t=120"
}
...
}
Fallback
Draggable
mousedown
→ mousemove
approach to let you reposition the popover if it obstructs something.Here is a complete example combining some of these features:
import {
WalkthroughProvider,
useWalkthrough,
type WalkthroughResponse,
} from "@sammy-labs/walkthroughs";
function App() {
return (
<WalkthroughProvider
token="..."
baseUrl="https://api.sammylabs.com"
locationChangeEvents={true}
locationChangePollInterval={500}
driverConfig={{
overlayOpacity: 0.5,
}}
config={{
debug: true,
overrideDomainUrl: "https://demo.yourapp.com",
askInput: false,
}}
>
<MyComponent />
</WalkthroughProvider>
);
}
function MyComponent() {
const { startWithData, stop, isActive } = useWalkthrough();
const runInformationalStep = async () => {
const sample: WalkthroughResponse = {
flow_id: "demo_flow",
history: [
{
step_num: 1,
step_type: "informational",
video_info: { youtube_url: "https://youtu.be/abc123" },
state: {
url: "https://demo.yourapp.com/page1",
interacted_element: [],
},
result: [],
model_output: {
action: [],
current_state: {
memory: "",
next_goal: "",
evaluation_previous_goal: "",
},
},
},
],
};
await startWithData(sample);
};
return (
<div>
<button onClick={runInformationalStep}>Start Informational Demo</button>
<button onClick={() => isActive() && stop()}>Stop Walkthrough</button>
</div>
);
}
"Why can't it find my element?"
xpath
or attributes) are stable. Turn on debug: true
to see logs. If the element is in a lazy-loaded modal, you may need extra time or increase elementFindTimeoutMs
."Can I override the domain in the steps?"
overrideDomainUrl: "https://demo.deel.com"
in your config. The library will reconstruct the final URL for each step using that domain."How do I customize how long the system waits for elements before creating fallbacks?"
fallbackTimeout
parameter directly in the useWalkthrough
hook options. For example: useWalkthrough({ fallbackTimeout: 15000 })
will wait 15 seconds before creating fallback elements. This parameter is separated from the main config object to avoid React re-render issues when used in effects."How do I pass a custom popup style or embed a custom button?"
popoverClass
or an onPopoverRender
callback in driverConfig
. For full control, you can modify the driver callbacks."Does it work outside of React?"
@sammy-labs/walkthroughs
is licensed under the MIT License. See the LICENSE file for more details.
FAQs
Sammy Labs Walkthroughs
The npm package @sammy-labs/walkthroughs receives a total of 0 weekly downloads. As such, @sammy-labs/walkthroughs popularity was classified as not popular.
We found that @sammy-labs/walkthroughs demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
/Research
Malicious npm package impersonates Nodemailer and drains wallets by hijacking crypto transactions across multiple blockchains.
Security News
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Security News
/Research
Malicious Nx npm versions stole secrets and wallet info using AI CLI tools; Socket’s AI scanner detected the supply chain attack and flagged the malware.