UNPKG

@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
"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