@revrag-ai/embed-react-native
Version:
A powerful React Native library for integrating AI-powered voice agents into mobile applications. Features real-time voice communication, intelligent speech processing, customizable UI components, and comprehensive event handling for building conversation
638 lines (585 loc) • 20.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.trackRageClick = exports.trackFormEvent = exports.trackEmbedEvent = exports.getRouteHierarchy = exports.getNavigationTree = exports.getAllRoutes = exports.getAllDefinedRoutes = exports.default = exports.EmbedProvider = void 0;
var _react = _interopRequireWildcard(require("react"));
var _EmbedButton = require("../components/Embed/EmbedButton.js");
var _embedEvent = _interopRequireWildcard(require("../events/embed.event.js"));
var _jsxRuntime = require("react/jsx-runtime");
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
/**
* @file EmbedProvider.tsx
* @description Core provider component for the Embed React Native library.
*
* This provider:
* - Tracks navigation state and screen changes
* - Conditionally renders the EmbedButton based on current screen
* - Provides utilities for extracting navigation hierarchy information
* - Manages event tracking integration with the Embed system
*
* @module EmbedProvider
*/
/* ============================================================================
* TYPE DEFINITIONS
* ========================================================================== */
/**
* Props for the EmbedProvider component
* @interface EmbedProviderProps
*/
/**
* Represents the complete hierarchical structure of the current navigation state
* @interface RouteHierarchy
*/
/**
* Represents a node in the navigation tree structure
* @interface NavigationTreeNode
*/
/* ============================================================================
* CONSTANTS
* ========================================================================== */
/**
* Default list of screens where the EmbedButton should be displayed
* Used when no custom includeScreens prop is provided
* @constant
*/
const DEFAULT_INCLUDED_SCREENS = [];
/**
* Delay before setting up navigation listener (allows navigation to stabilize)
* @constant
*/
const NAVIGATION_LISTENER_DELAY = 1000;
/* ============================================================================
* UTILITY FUNCTIONS - NAVIGATION HIERARCHY
* ========================================================================== */
/**
* Extracts complete route hierarchy information from navigation state
*
* This function traverses the navigation state tree recursively to build
* a comprehensive hierarchy object containing the current screen, full path,
* depth, and route information at each level.
*
* @param {any} state - Navigation state from navigationRef.current?.getRootState()
* @returns {RouteHierarchy | null} Complete route hierarchy or null if state is invalid
*
* @example
* ```typescript
* const routeInfo = getRouteHierarchy(navigationRef.current?.getRootState());
* console.log(routeInfo);
* // {
* // currentScreen: "Product",
* // fullPath: "MainApp > Home > Product",
* // allRoutes: ["MainApp", "Home", "Product"],
* // depth: 3,
* // routesByLevel: { 0: "MainApp", 1: "Home", 2: "Product" },
* // parentRoute: "Home",
* // routeParams: { id: "123" }
* // }
* ```
*/
const getRouteHierarchy = state => {
if (!state) return null;
try {
const allRoutes = [];
const routesByLevel = {};
let currentRouteParams;
/**
* Recursively traverse the navigation state tree
* @param {any} currentState - Current state node
* @param {number} level - Current depth level (0 = root)
*/
const traverse = (currentState, level = 0) => {
if (!currentState || typeof currentState.index !== 'number') return;
const route = currentState.routes?.[currentState.index];
if (!route || !route.name) return;
const routeName = route.name;
allRoutes.push(routeName);
routesByLevel[level] = routeName;
// Capture parameters if this is the deepest (active) route
if (!route.state && route.params) {
currentRouteParams = route.params;
}
// Recursively traverse nested navigation states
if (route.state) {
traverse(route.state, level + 1);
}
};
// Start traversal from root
traverse(state);
// Return null if no routes were found
if (allRoutes.length === 0) return null;
// Build the hierarchy object
const currentScreen = allRoutes[allRoutes.length - 1] || 'Unknown';
const parentRoute = allRoutes.length > 1 ? allRoutes[allRoutes.length - 2] : undefined;
const depth = allRoutes.length;
const fullPath = allRoutes.join(' > ');
return {
currentScreen,
fullPath,
allRoutes,
depth,
routesByLevel,
parentRoute,
routeParams: currentRouteParams
};
} catch (error) {
// Return null if any error occurs during traversal
return null;
}
};
/**
* Extracts ALL available routes (both mounted and defined) from navigation state
*
* This function traverses the navigation state and returns a flat array of:
* 1. Currently MOUNTED routes (routes that are rendered in the navigation tree)
* 2. DEFINED routes (from routeNames - routes that exist but may not be rendered yet)
*
* This is particularly useful because nested navigators (like tab navigators)
* won't appear in the state until you navigate to them, but their route names
* are still defined in the navigator configuration.
*
* @param {any} state - Navigation state from navigationRef.current?.getRootState()
* @returns {string[]} Array of unique route names
*
* @example
* ```typescript
* const allRoutes = getAllRoutes(navigationRef.current?.getRootState());
* console.log(allRoutes);
* // ["Splash", "Welcome", "MainApp", "Home", "Transactions", "Profile"]
*
* // Check if a specific route is available
* const hasProfileScreen = allRoutes.includes("Profile");
* ```
*/
exports.getRouteHierarchy = getRouteHierarchy;
const getAllRoutes = state => {
if (!state) return [];
// Use Set for O(1) lookup instead of O(n) with array.includes
const allRouteNamesSet = new Set();
/**
* Recursively collect route names from navigation state
* @param {any} currentState - Current state node
*/
const collectRoutes = currentState => {
if (!currentState) return;
// Collect defined route names (ALL routes configured in the navigator)
if (currentState?.routeNames && Array.isArray(currentState.routeNames)) {
currentState.routeNames.forEach(routeName => {
if (routeName && typeof routeName === 'string') {
allRouteNamesSet.add(routeName);
}
});
}
// Collect mounted routes (currently rendered routes)
if (!currentState?.routes || !Array.isArray(currentState.routes)) return;
currentState.routes.forEach(route => {
if (route?.name && typeof route.name === 'string') {
allRouteNamesSet.add(route.name);
}
// Recursively check nested states (for mounted nested navigators)
if (route?.state) {
collectRoutes(route.state);
}
});
};
try {
collectRoutes(state);
} catch (error) {
// Fail silently and return empty array if state traversal fails
return [];
}
return Array.from(allRouteNamesSet);
};
/**
* Builds a complete navigation tree structure showing all routes and their relationships
*
* This function creates a hierarchical tree structure representing the entire
* navigation state, including all routes at all levels (not just the active path).
* Useful for visualizing or debugging your navigation structure.
*
* @param {any} state - Navigation state from navigationRef.current?.getRootState()
* @returns {NavigationTreeNode[]} Array of NavigationTreeNode representing the complete tree
*
* @example
* ```typescript
* const tree = getNavigationTree(navigationRef.current?.getRootState());
* console.log(JSON.stringify(tree, null, 2));
*
* // Example output:
* // [
* // {
* // "name": "MainApp",
* // "level": 0,
* // "children": [
* // {
* // "name": "Home",
* // "level": 1,
* // "children": [
* // { "name": "HomeMain", "level": 2 },
* // { "name": "Product", "level": 2, "params": { "id": "123" } }
* // ]
* // },
* // { "name": "Profile", "level": 1 }
* // ]
* // }
* // ]
* ```
*/
exports.getAllRoutes = getAllRoutes;
const getNavigationTree = state => {
if (!state) return [];
/**
* Recursively build the navigation tree
* @param {any} currentState - Current state node
* @param {number} level - Current depth level (0 = root)
* @returns {NavigationTreeNode[]} Array of tree nodes
*/
const buildTree = (currentState, level = 0) => {
if (!currentState?.routes || !Array.isArray(currentState.routes)) {
return [];
}
return currentState.routes.filter(route => route && route.name).map(route => {
const node = {
name: route.name,
level,
params: route.params
};
// Recursively build children if nested state exists
if (route.state) {
node.children = buildTree(route.state, level + 1);
}
return node;
});
};
try {
return buildTree(state);
} catch (error) {
// Return empty array if tree building fails
return [];
}
};
/**
* Legacy alias for getAllRoutes()
* @deprecated Use getAllRoutes() instead
*
* Note: React Navigation limitation - nested navigator routes (like tabs inside a stack)
* only appear in the state AFTER they're mounted/rendered.
*
* @param {any} state - Navigation state
* @returns {string[]} Array of route names
*/
exports.getNavigationTree = getNavigationTree;
const getAllDefinedRoutes = state => {
return getAllRoutes(state);
};
/* ============================================================================
* UTILITY FUNCTIONS - APP VERSION
* ========================================================================== */
/**
* Interface for app version information
* @interface AppVersionInfo
*/
exports.getAllDefinedRoutes = getAllDefinedRoutes;
/* ============================================================================
* UTILITY FUNCTIONS - EVENT TRACKING
* ========================================================================== */
/**
* Track custom events with the Embed system
*
* This function provides a simple interface to send custom event data
* to the Embed analytics system. All errors are caught and handled silently
* to prevent analytics issues from affecting app functionality.
*
* @param {string} eventName - Name of the event to track
* @param {Record<string, any>} properties - Event properties/metadata (optional)
*
* @example
* ```typescript
* // Track a button click
* trackEmbedEvent('button_clicked', {
* buttonId: 'submit_form',
* screenName: 'Profile',
* });
*
* // Track a purchase
* trackEmbedEvent('purchase_completed', {
* amount: 99.99,
* currency: 'USD',
* items: ['item1', 'item2'],
* });
* ```
*/
const trackEmbedEvent = (eventName, properties = {}) => {
try {
_embedEvent.default.Event(_embedEvent.EventKeys.FORM_STATE, {
type: 'custom_event',
eventName,
properties,
timestamp: new Date().toISOString()
});
} catch (error) {
// Fail silently - analytics errors should not break the app
// You could log to a monitoring service here if needed
}
};
/**
* Track rage clicks (repeated rapid clicks indicating user frustration)
*
* Rage clicks are detected when a user clicks the same element multiple times
* in rapid succession, which can indicate frustration with unresponsive UI.
* This data helps identify problem areas in the user experience.
*
* @param {string} elementId - Unique identifier of the clicked element
* @param {string} elementType - Type of element (e.g., 'button', 'link', 'text')
* @param {{ x: number; y: number }} coordinates - Screen coordinates of the clicks
* @param {number} clickCount - Number of rapid clicks detected
*
* @example
* ```typescript
* trackRageClick('submit_button', 'button', { x: 100, y: 200 }, 5);
* ```
*/
exports.trackEmbedEvent = trackEmbedEvent;
const trackRageClick = (elementId, elementType, coordinates, clickCount) => {
try {
const eventData = {
type: 'rage_click',
elementId,
elementType,
coordinates,
clickCount,
timestamp: new Date().toISOString()
};
_embedEvent.default.Event(_embedEvent.EventKeys.FORM_STATE, eventData);
} catch (error) {
// Fail silently - analytics errors should not break the app
}
};
/**
* Track form-related events (changes, submissions, errors)
*
* This function tracks various form interactions to help understand
* user behavior and identify potential issues in form flows.
*
* @param {string} formId - Unique identifier of the form
* @param {'form_change' | 'form_submit' | 'form_error'} eventType - Type of form event
* @param {Record<string, any>} formData - Form data/metadata (optional)
*
* @example
* ```typescript
* // Track form field change
* trackFormEvent('signup_form', 'form_change', {
* field: 'email',
* value: 'user@example.com',
* });
*
* // Track form submission
* trackFormEvent('signup_form', 'form_submit', {
* success: true,
* userId: '12345',
* });
*
* // Track form error
* trackFormEvent('signup_form', 'form_error', {
* field: 'password',
* error: 'Password too short',
* });
* ```
*/
exports.trackRageClick = trackRageClick;
const trackFormEvent = (formId, eventType, formData = {}) => {
try {
const eventData = {
type: eventType,
formId,
formData,
timestamp: new Date().toISOString()
};
_embedEvent.default.Event(_embedEvent.EventKeys.FORM_STATE, eventData);
} catch (error) {
// Fail silently - analytics errors should not break the app
}
};
/* ============================================================================
* MAIN COMPONENT
* ========================================================================== */
/**
* EmbedProvider Component
*
* Core provider component that wraps your application to enable Embed functionality.
* This provider:
* - Monitors navigation state changes
* - Automatically shows/hides the EmbedButton based on current screen
* - Tracks screen views and navigation hierarchy
* - Integrates with the Embed analytics system
*
* @param {EmbedProviderProps} props - Component props
* @returns {JSX.Element} Provider component with children and conditional EmbedButton
*
* @example
* ```typescript
* import { NavigationContainer } from '@react-navigation/native';
* import { EmbedProvider } from '@revrag/embed-react-native';
* import { useRef } from 'react';
*
* function App() {
* const navigationRef = useRef<NavigationContainerRef<any>>(null);
*
* return (
* // IMPORTANT: EmbedProvider must WRAP NavigationContainer
* <EmbedProvider
* navigationRef={navigationRef}
* includeScreens={['Home', 'Profile', 'Dashboard']}
* >
* <NavigationContainer ref={navigationRef}>
* {/* Your navigation stack *\/}
* </NavigationContainer>
* </EmbedProvider>
* );
* }
* ```
*/
exports.trackFormEvent = trackFormEvent;
const EmbedProvider = ({
children,
navigationRef,
includeScreens = DEFAULT_INCLUDED_SCREENS,
appVersion: _appVersion
}) => {
// ========== Refs ==========
/**
* Navigation reference - use provided ref or create a default one
* This allows the provider to work both with and without an external navigation ref
* Note: Using useRef instead of createRef to maintain same ref across renders
*/
const defaultNavRef = (0, _react.useRef)(null);
const navRef = navigationRef || defaultNavRef;
/**
* Track if component is mounted to prevent state updates after unmount
*/
const isMountedRef = (0, _react.useRef)(true);
// ========== State ==========
/** Whether the EmbedButton should be visible on the current screen */
const [showEmbedButton, setShowEmbedButton] = (0, _react.useState)(false);
// ========== Memoized Values ==========
/**
* Included screens (from prop)
* Memoized to prevent recalculation on every render and filter falsy values
*/
const allIncludedScreens = (0, _react.useMemo)(() => {
return Array.from(new Set(includeScreens.filter(Boolean)));
}, [includeScreens]);
// ========== Effects ==========
/**
* Memoized callback for tracking screen changes
* Uses useCallback to maintain stable reference and prevent unnecessary re-renders
*/
const trackScreen = (0, _react.useCallback)(async () => {
try {
// Safety check: Ensure navigation ref is available
if (!navRef.current) {
return;
}
const state = navRef.current.getRootState();
if (!state) {
return;
}
const routeInfo = getRouteHierarchy(state);
// Extract current screen name from navigation state
const screenName = routeInfo?.currentScreen || navRef.current.getCurrentRoute()?.name || '';
// Determine if EmbedButton should be visible
// Show button only on screens specified in includeScreens (prop + backend)
const hasIncludeFilter = allIncludedScreens.length > 0;
const isIncluded = allIncludedScreens.includes(screenName);
const shouldShowButton = screenName !== '' && (!hasIncludeFilter || isIncluded);
// Only update state if component is still mounted
if (isMountedRef.current) {
setShowEmbedButton(shouldShowButton);
}
// Track screen view event (if route info is available)
if (routeInfo) {
try {
// TODO: Uncomment when ready to enable screen view tracking
_embedEvent.default.Event(_embedEvent.EventKeys.FORM_STATE, {
type: 'screen_view',
...routeInfo,
screen: routeInfo.currentScreen,
// allAvailableRoutes: allRoutes,
app_version: _appVersion,
timestamp: new Date().toISOString()
});
} catch (error) {
// Silently fail - analytics should not break the app
}
}
} catch (error) {
// Catch-all error handler to prevent any tracking errors from breaking the app
}
}, [navRef, allIncludedScreens, _appVersion]);
/**
* Effect: Navigation State Tracking
*
* Sets up listeners for navigation state changes and tracks:
* - Current screen name
* - Route hierarchy
* - Screen view events
* - EmbedButton visibility
*/
(0, _react.useEffect)(() => {
// Validate navigation ref
if (!navRef) {
return;
}
let unsubscribe;
let delayTimer;
/**
* Set up navigation state listener
*/
const setupListener = () => {
// Only set up if navigation ref is ready
if (navRef.current) {
unsubscribe = navRef.current.addListener('state', trackScreen);
trackScreen(); // Track initial screen immediately
}
};
// Set up listener immediately for quick feedback
setupListener();
// Also set up after delay in case navigation wasn't ready
delayTimer = setTimeout(() => {
if (!unsubscribe && navRef.current) {
setupListener();
}
}, NAVIGATION_LISTENER_DELAY);
// Cleanup function
return () => {
if (delayTimer) {
clearTimeout(delayTimer);
}
if (unsubscribe) {
unsubscribe();
}
};
}, [navRef, trackScreen]);
/**
* Effect: Component Lifecycle Management
*
* Tracks component mount/unmount state to prevent state updates after unmount
*/
(0, _react.useEffect)(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
// ========== Render ==========
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
children: [children, showEmbedButton && /*#__PURE__*/(0, _jsxRuntime.jsx)(_EmbedButton.EmbedButton, {})]
});
};
/* ============================================================================
* EXPORTS
* ========================================================================== */
// Default export
exports.EmbedProvider = EmbedProvider;
var _default = exports.default = EmbedProvider;
//# sourceMappingURL=EmbedProvider.js.map