/******************************************************************************************** * Copyright (C) 2025 Acoustic, L.P. All rights reserved. * * NOTICE: This file contains material that is confidential and proprietary to * Acoustic, L.P. and/or other developers. No license is granted under any intellectual or * industrial property rights of Acoustic, L.P. except as may be provided in an agreement with * Acoustic, L.P. Any unauthorized copying or distribution of content from this file is * prohibited. * * Created by Omar Hernandez on 5/9/25. * ********************************************************************************************/ import React, { forwardRef, useImperativeHandle, useRef, useEffect, useCallback } from 'react'; import { Alert } from 'react-native'; import { useDialogTracking } from './useDialogTracking'; import AcousticConnectRN from '../index'; /** * HOC Wrapper that automatically tracks dialog show/dismiss events and button clicks * Works with any dialog component that has 'visible' and 'onDismiss' props * * Supported Dialog Patterns: * - react-native-paper Dialog components * - Custom modal components with visible/onDismiss props * - Components with different prop names (show/hide, open/close, etc.) * - Components with custom button implementations * - Components with nested dialog structures */ export function withAcousticAutoDialog( DialogComponent: React.ComponentType ): React.ComponentType { return forwardRef((props, ref) => { const { generateDialogId } = useDialogTracking(); const dialogIdRef = useRef(null); const isVisibleRef = useRef(false); // Track button click event const trackButtonClick = useCallback((buttonText: string, buttonIndex: number) => { if (dialogIdRef.current) { AcousticConnectRN.logDialogButtonClickEvent(dialogIdRef.current, buttonText, buttonIndex); console.log(`🔍 withAcousticAutoDialog: Button clicked - ${buttonText} (${dialogIdRef.current})`); } }, []); // Wrap button onPress handlers to track clicks const wrapButtonOnPress = useCallback((originalOnPress: (() => void) | undefined, buttonText: string, buttonIndex: number) => { return () => { trackButtonClick(buttonText, buttonIndex); if (originalOnPress) { originalOnPress(); } }; }, [trackButtonClick]); // Recursively wrap buttons in children const wrapButtonsInChildren = useCallback((children: any, buttonIndex: number = 0): any => { if (!children) return children; if (Array.isArray(children)) { return children.map((child, index) => wrapButtonsInChildren(child, buttonIndex + index)); } if (React.isValidElement(children)) { const childProps = children.props as any; // Check if this is a button component (supports various button types) const isButton = (typeof children.type === 'function' && ((children.type as any).displayName === 'Button' || (children.type as any).name === 'Button' || (children.type as any).displayName === 'DialogAction' || (children.type as any).name === 'DialogAction' || (children.type as any).displayName === 'TouchableOpacity' || (children.type as any).name === 'TouchableOpacity' || (children.type as any).displayName === 'Pressable' || (children.type as any).name === 'Pressable')) || (childProps && (childProps.onPress || childProps.onPressIn || childProps.onPressOut || childProps.onTouchEnd)); if (isButton && childProps?.onPress) { const buttonText = childProps.children || childProps.title || 'Button'; return React.cloneElement(children, { ...childProps, onPress: wrapButtonOnPress(childProps.onPress, buttonText, buttonIndex) } as any); } // Recursively wrap buttons in nested children if (childProps?.children) { return React.cloneElement(children, { ...childProps, children: wrapButtonsInChildren(childProps.children, buttonIndex) } as any); } } return children; }, [wrapButtonOnPress]); // Track dialog show event when visible changes to true // Supports various visibility prop names: visible, show, open, isOpen, isVisible const isVisible = props.visible || props.show || props.open || props.isOpen || props.isVisible; useEffect(() => { if (isVisible && !isVisibleRef.current) { const dialogId = generateDialogId(); dialogIdRef.current = dialogId; isVisibleRef.current = true; // Get dialog title from various possible sources const getDialogTitle = (): string => { // Try to get title from props (supports various prop names) if (props.title) return props.title; if (props.dialogTitle) return props.dialogTitle; if (props.header) return props.header; if (props.heading) return props.heading; if (props.name) return props.name; // Try to get title from children (for react-native-paper Dialog) if (props.children) { console.log(`🔍 withAcousticAutoDialog: Starting title extraction from children`); console.log(`🔍 withAcousticAutoDialog: Initial children:`, props.children); const extractTitleFromChildren = (children: any, depth: number = 0): string | null => { if (!children) return null; if (Array.isArray(children)) { for (const child of children) { const title = extractTitleFromChildren(child, depth + 1); if (title) return title; } return null; } if (React.isValidElement(children)) { const childProps = children.props as any; // Debug: Log component info for debugging console.log(`🔍 withAcousticAutoDialog: Processing component at depth ${depth}:`, { type: children.type, displayName: (children.type as any)?.displayName, name: (children.type as any)?.name, hasChildren: !!childProps?.children, childrenType: typeof childProps?.children }); // Check if this is a Dialog component from react-native-paper if (children.type && typeof children.type === 'function' && ((children.type as any).displayName === 'Dialog' || (children.type as any).name === 'Dialog')) { console.log(`🔍 withAcousticAutoDialog: Found Dialog component at depth ${depth}`); // Extract title from Dialog children if (childProps?.children) { return extractTitleFromChildren(childProps.children, depth + 1); } } // Check if this is a Portal component (common wrapper for dialogs) if (children.type && typeof children.type === 'function' && ((children.type as any).displayName === 'Portal' || (children.type as any).name === 'Portal')) { console.log(`🔍 withAcousticAutoDialog: Found Portal component at depth ${depth}`); // Extract title from Portal children if (childProps?.children) { return extractTitleFromChildren(childProps.children, depth + 1); } } // Check if this is a DialogTitle component (supports various title component types) if (children.type && typeof children.type === 'function' && ((children.type as any).displayName === 'DialogTitle' || (children.type as any).name === 'DialogTitle' || (children.type as any).displayName === 'Title' || (children.type as any).name === 'Title' || (children.type as any).displayName === 'Header' || (children.type as any).name === 'Header' || (children.type as any).displayName === 'Heading' || (children.type as any).name === 'Heading')) { if (childProps?.children && typeof childProps.children === 'string') { console.log(`🔍 withAcousticAutoDialog: Found title in title component: "${childProps.children}"`); return childProps.children; } } // Check if this component has title content if (childProps?.title) { return childProps.title; } // Recursively search in nested children if (childProps?.children) { return extractTitleFromChildren(childProps.children, depth + 1); } } return null; }; const title = extractTitleFromChildren(props.children); if (title) { console.log(`🔍 withAcousticAutoDialog: Found title from children: "${title}"`); return title; } } return 'Dialog'; }; const title = getDialogTitle(); // Log dialog show event AcousticConnectRN.logDialogShowEvent(dialogId, title, 'custom'); console.log(`🔍 withAcousticAutoDialog: Dialog shown - ${title} (${dialogId})`); } else if (!isVisible && isVisibleRef.current) { // Track dialog dismiss event when visible changes to false if (dialogIdRef.current) { AcousticConnectRN.logDialogDismissEvent(dialogIdRef.current, 'user_action'); console.log(`🔍 withAcousticAutoDialog: Dialog dismissed - ${dialogIdRef.current}`); dialogIdRef.current = null; } isVisibleRef.current = false; } }, [isVisible, generateDialogId]); // Handle dismiss events (supports various dismiss prop names) const handleDismiss = () => { if (dialogIdRef.current) { AcousticConnectRN.logDialogDismissEvent(dialogIdRef.current, 'user_action'); console.log(`🔍 withAcousticAutoDialog: Dialog dismissed via dismiss handler - ${dialogIdRef.current}`); dialogIdRef.current = null; } isVisibleRef.current = false; // Call original dismiss handler if it exists (supports various prop names) if (props.onDismiss) { props.onDismiss(); } else if (props.onClose) { props.onClose(); } else if (props.onHide) { props.onHide(); } else if (props.close) { props.close(); } else if (props.hide) { props.hide(); } }; // Forward ref to the wrapped component useImperativeHandle(ref, () => { return { ...ref, // Add any additional methods if needed }; }, [ref]); // Wrap buttons in children to track clicks const wrappedChildren = wrapButtonsInChildren(props.children); // Determine which dismiss prop to use based on what the original component expects const dismissProps: any = {}; if (props.onDismiss) { dismissProps.onDismiss = handleDismiss; } else if (props.onClose) { dismissProps.onClose = handleDismiss; } else if (props.onHide) { dismissProps.onHide = handleDismiss; } else if (props.close) { dismissProps.close = handleDismiss; } else if (props.hide) { dismissProps.hide = handleDismiss; } else { // Default to onDismiss if no dismiss prop is found dismissProps.onDismiss = handleDismiss; } return ( ); }); } /** * Convenience function to create tracked versions of common dialog components */ export function createTrackedDialogComponents() { // Note: These would need to be imported in the consuming app // This is just a template for how to use the HOC return { // Example usage (uncomment when react-native-paper is available): // TrackedDialog: withAcousticAutoDialog(require('react-native-paper').Dialog), // TrackedPortal: withAcousticAutoDialog(require('react-native-paper').Portal), }; } /** * Hook to get tracked dialog components */ export function useTrackedDialogs() { return { withAcousticAutoDialog, createTrackedDialogComponents, }; }