@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
209 lines (196 loc) • 6.75 kB
JavaScript
;
import { useState, useEffect, useCallback, memo } from 'react';
import { View, StyleSheet } from 'react-native';
import ErrorBoundary from '../components/ErrorBoundary';
// Import types and route registry
import { routes, routeNames } from './routes';
import { jsx as _jsx } from "react/jsx-runtime";
// Helper function to validate route names at runtime
const isValidRouteName = screen => {
return routeNames.includes(screen);
};
// Helper function for safe navigation with validation
const validateAndNavigate = (screen, props, setCurrentScreen, setScreenHistory, setScreenPropsMap) => {
if (!isValidRouteName(screen)) {
const errorMsg = `Invalid route name: "${screen}". Valid routes are: ${routeNames.join(', ')}`;
console.error('OxyRouter:', errorMsg);
if (process.env.NODE_ENV !== 'production') {
console.error('Navigation error:', errorMsg);
}
return false;
}
if (!routes[screen]) {
const errorMsg = `Route "${screen}" is registered but component is missing`;
console.error('OxyRouter:', errorMsg);
if (process.env.NODE_ENV !== 'production') {
console.error('Navigation error:', errorMsg);
}
return false;
}
return true;
};
const OxyRouter = ({
oxyServices,
initialScreen,
onClose,
onAuthenticated,
theme,
adjustSnapPoints,
navigationRef,
containerWidth
}) => {
const [currentScreen, setCurrentScreen] = useState(initialScreen);
const [screenHistory, setScreenHistory] = useState([initialScreen]);
// Store props per screen for correct restoration on back
const [screenPropsMap, setScreenPropsMap] = useState({
[initialScreen]: {}
});
// Update snap points when the screen changes
useEffect(() => {
if (routes[currentScreen] && typeof adjustSnapPoints === 'function') {
adjustSnapPoints(routes[currentScreen].snapPoints);
}
}, [currentScreen, adjustSnapPoints]);
// Memoized navigation methods with validation
const navigate = useCallback((screen, props = {}) => {
if (__DEV__) console.log('OxyRouter: navigate called', screen, props);
// Validate route before navigating
if (!validateAndNavigate(screen, props, setCurrentScreen, setScreenHistory, setScreenPropsMap)) {
return; // Early return if validation fails
}
// All validations passed, proceed with navigation
setCurrentScreen(screen);
setScreenHistory(prev => [...prev, screen]);
setScreenPropsMap(prev => ({
...prev,
[screen]: props
}));
}, []);
const goBack = useCallback(() => {
setScreenHistory(prev => {
if (prev.length > 1) {
const newHistory = [...prev];
newHistory.pop();
const previousScreen = newHistory[newHistory.length - 1];
setCurrentScreen(previousScreen);
return newHistory;
} else {
if (onClose) onClose();
return prev;
}
});
}, [onClose]);
// Expose the navigate function to the parent component
useEffect(() => {
if (navigationRef) {
navigationRef.current = navigate;
if (__DEV__) console.log('OxyRouter: navigationRef set');
}
return () => {
if (navigationRef) {
navigationRef.current = null;
if (__DEV__) console.log('OxyRouter: navigationRef cleared');
}
};
}, [navigate, navigationRef]);
// Expose the navigate method to the parent component (OxyProvider)
useEffect(() => {
const handleNavigationEvent = event => {
if (event && event.detail) {
if (typeof event.detail === 'string') {
// Validate string route name before navigating
if (isValidRouteName(event.detail)) {
navigate(event.detail);
} else {
console.error('OxyRouter: Invalid route name in event:', event.detail);
}
} else if (typeof event.detail === 'object' && event.detail.screen) {
const {
screen,
props
} = event.detail;
// Validate route name before navigating
if (isValidRouteName(screen)) {
navigate(screen, props || {});
} else {
console.error('OxyRouter: Invalid route name in event:', screen);
}
}
}
};
let intervalId = null;
if (typeof document !== 'undefined' && document.addEventListener) {
document.addEventListener('oxy:navigate', handleNavigationEvent);
} else {
intervalId = setInterval(() => {
const globalNav = globalThis.oxyNavigateEvent;
if (globalNav && globalNav.screen) {
// Validate route name before navigating
if (isValidRouteName(globalNav.screen)) {
navigate(globalNav.screen, globalNav.props || {});
globalThis.oxyNavigateEvent = null;
} else {
console.error('OxyRouter: Invalid route name in global event:', globalNav.screen);
globalThis.oxyNavigateEvent = null; // Clear invalid event
}
}
}, 100);
}
return () => {
if (typeof document !== 'undefined' && document.removeEventListener) {
document.removeEventListener('oxy:navigate', handleNavigationEvent);
}
if (intervalId) {
clearInterval(intervalId);
}
};
}, [navigate]);
// Render the current screen component with error boundary
const renderScreen = () => {
const CurrentScreen = routes[currentScreen]?.component;
if (!CurrentScreen) {
if (process.env.NODE_ENV !== 'production') {
console.error(`Screen "${currentScreen}" not found`);
}
return /*#__PURE__*/_jsx(View, {
style: styles.errorContainer
});
}
return /*#__PURE__*/_jsx(ErrorBoundary, {
onError: (error, errorInfo) => {
console.error(`Error in screen "${currentScreen}":`, error, errorInfo);
},
children: /*#__PURE__*/_jsx(CurrentScreen, {
oxyServices: oxyServices,
navigate: navigate,
goBack: goBack,
onClose: onClose,
onAuthenticated: onAuthenticated,
theme: theme,
containerWidth: containerWidth,
...(screenPropsMap[currentScreen] || {})
})
});
};
return /*#__PURE__*/_jsx(View, {
style: styles.container,
children: renderScreen()
});
};
// Memoize the router component to prevent unnecessary re-renders
const MemoizedOxyRouter = /*#__PURE__*/memo(OxyRouter);
const styles = StyleSheet.create({
container: {
flex: 1
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
minHeight: 100
}
});
// Export both the memoized version (default) and the original for testing
export { OxyRouter };
export default MemoizedOxyRouter;
//# sourceMappingURL=OxyRouter.js.map