@buoy-gg/route-events 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -0
- package/lib/commonjs/RouteObserver.js +54 -0
- package/lib/commonjs/RouteParser.js +310 -0
- package/lib/commonjs/RouteTracker.js +39 -0
- package/lib/commonjs/components/NavigationStack.js +584 -0
- package/lib/commonjs/components/RouteEventDetailContent.js +492 -0
- package/lib/commonjs/components/RouteEventExpandedContent.js +187 -0
- package/lib/commonjs/components/RouteEventItemCompact.js +175 -0
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +560 -0
- package/lib/commonjs/components/RouteEventsTimeline.js +82 -0
- package/lib/commonjs/components/RouteFilterViewV2.js +42 -0
- package/lib/commonjs/components/RoutesSitemap.js +948 -0
- package/lib/commonjs/expoRouterStore.js +104 -0
- package/lib/commonjs/index.js +99 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +83 -0
- package/lib/commonjs/useNavigationStack.js +241 -0
- package/lib/commonjs/useRouteObserver.js +73 -0
- package/lib/commonjs/useRouteSitemap.js +234 -0
- package/lib/commonjs/utils/safeExpoRouter.js +129 -0
- package/lib/commonjs/utils/safeReactNavigation.js +104 -0
- package/lib/module/RouteObserver.js +49 -0
- package/lib/module/RouteParser.js +305 -0
- package/lib/module/RouteTracker.js +35 -0
- package/lib/module/components/NavigationStack.js +580 -0
- package/lib/module/components/RouteEventDetailContent.js +487 -0
- package/lib/module/components/RouteEventExpandedContent.js +183 -0
- package/lib/module/components/RouteEventItemCompact.js +171 -0
- package/lib/module/components/RouteEventsModalWithTabs.js +557 -0
- package/lib/module/components/RouteEventsTimeline.js +78 -0
- package/lib/module/components/RouteFilterViewV2.js +38 -0
- package/lib/module/components/RoutesSitemap.js +944 -0
- package/lib/module/expoRouterStore.js +98 -0
- package/lib/module/index.js +23 -0
- package/lib/module/preset.js +79 -0
- package/lib/module/useNavigationStack.js +238 -0
- package/lib/module/useRouteObserver.js +70 -0
- package/lib/module/useRouteSitemap.js +229 -0
- package/lib/module/utils/safeExpoRouter.js +120 -0
- package/lib/module/utils/safeReactNavigation.js +98 -0
- package/lib/typescript/RouteObserver.d.ts +37 -0
- package/lib/typescript/RouteObserver.d.ts.map +1 -0
- package/lib/typescript/RouteParser.d.ts +129 -0
- package/lib/typescript/RouteParser.d.ts.map +1 -0
- package/lib/typescript/RouteTracker.d.ts +29 -0
- package/lib/typescript/RouteTracker.d.ts.map +1 -0
- package/lib/typescript/components/NavigationStack.d.ts +11 -0
- package/lib/typescript/components/NavigationStack.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventDetailContent.d.ts +21 -0
- package/lib/typescript/components/RouteEventDetailContent.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventExpandedContent.d.ts +16 -0
- package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventItemCompact.d.ts +15 -0
- package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts +15 -0
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventsTimeline.d.ts +17 -0
- package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -0
- package/lib/typescript/components/RouteFilterViewV2.d.ts +9 -0
- package/lib/typescript/components/RouteFilterViewV2.d.ts.map +1 -0
- package/lib/typescript/components/RoutesSitemap.d.ts +15 -0
- package/lib/typescript/components/RoutesSitemap.d.ts.map +1 -0
- package/lib/typescript/expoRouterStore.d.ts +28 -0
- package/lib/typescript/expoRouterStore.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +18 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +76 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/lib/typescript/useNavigationStack.d.ts +48 -0
- package/lib/typescript/useNavigationStack.d.ts.map +1 -0
- package/lib/typescript/useRouteObserver.d.ts +27 -0
- package/lib/typescript/useRouteObserver.d.ts.map +1 -0
- package/lib/typescript/useRouteSitemap.d.ts +102 -0
- package/lib/typescript/useRouteSitemap.d.ts.map +1 -0
- package/lib/typescript/utils/safeExpoRouter.d.ts +13 -0
- package/lib/typescript/utils/safeExpoRouter.d.ts.map +1 -0
- package/lib/typescript/utils/safeReactNavigation.d.ts +10 -0
- package/lib/typescript/utils/safeReactNavigation.d.ts.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
let cachedStore = null;
|
|
4
|
+
let cachedStoreSource = null;
|
|
5
|
+
let importError = null;
|
|
6
|
+
let hasLoggedMissingStore = false;
|
|
7
|
+
let hasLoggedMissingRouteNode = false;
|
|
8
|
+
let lastRouteNodeTimestamp = null;
|
|
9
|
+
function logOnce(message, error) {
|
|
10
|
+
if (__DEV__) {
|
|
11
|
+
if (error) {
|
|
12
|
+
console.error(`[RouteEvents] ${message}`, error);
|
|
13
|
+
} else {
|
|
14
|
+
console.warn(`[RouteEvents] ${message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Attempt to require the expo-router store from known locations.
|
|
21
|
+
* Returns null (with a logged error in dev) when expo-router is not available.
|
|
22
|
+
*
|
|
23
|
+
* Note: The store uses getters that read from an internal storeRef.
|
|
24
|
+
* The storeRef gets populated when Expo Router's useStore() hook runs.
|
|
25
|
+
* So we cache the store reference but its property values update over time.
|
|
26
|
+
*/
|
|
27
|
+
export function getExpoRouterStore() {
|
|
28
|
+
if (cachedStore) {
|
|
29
|
+
return cachedStore;
|
|
30
|
+
}
|
|
31
|
+
const loadFromBuild = () => {
|
|
32
|
+
try {
|
|
33
|
+
const module = require("expo-router/build/global-state/router-store");
|
|
34
|
+
if (module?.store) {
|
|
35
|
+
cachedStore = module.store;
|
|
36
|
+
cachedStoreSource = "build";
|
|
37
|
+
importError = null;
|
|
38
|
+
hasLoggedMissingStore = false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
importError = error;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
};
|
|
46
|
+
const loadFromSrc = () => {
|
|
47
|
+
try {
|
|
48
|
+
const module = require("expo-router/src/global-state/router-store");
|
|
49
|
+
if (module?.store) {
|
|
50
|
+
cachedStore = module.store;
|
|
51
|
+
cachedStoreSource = "src";
|
|
52
|
+
importError = null;
|
|
53
|
+
hasLoggedMissingStore = false;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
importError = error;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
if (loadFromBuild() || loadFromSrc()) {
|
|
62
|
+
return cachedStore;
|
|
63
|
+
}
|
|
64
|
+
if (!hasLoggedMissingStore) {
|
|
65
|
+
logOnce("Unable to load expo-router internals. @buoy-gg/route-events requires expo-router >= 2.0.0. Install expo-router and ensure it is configured before using the Routes tab.", importError ?? undefined);
|
|
66
|
+
hasLoggedMissingStore = true;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns the current RouteNode tree (if available).
|
|
73
|
+
* Logs a helpful warning in development when the tree is missing even though
|
|
74
|
+
* the navigation ref is ready.
|
|
75
|
+
*/
|
|
76
|
+
export function loadRouteNode() {
|
|
77
|
+
const store = getExpoRouterStore();
|
|
78
|
+
if (!store) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (store.routeNode) {
|
|
82
|
+
hasLoggedMissingRouteNode = false;
|
|
83
|
+
lastRouteNodeTimestamp = Date.now();
|
|
84
|
+
return store.routeNode;
|
|
85
|
+
}
|
|
86
|
+
const isReady = store.navigationRef?.isReady?.();
|
|
87
|
+
if (__DEV__ && !hasLoggedMissingRouteNode && isReady) {
|
|
88
|
+
logOnce("Expo Router route tree is unavailable. Ensure your app directory is configured and that expo-router is initialized before opening the Route Events devtool.");
|
|
89
|
+
hasLoggedMissingRouteNode = true;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
export function getRouteNodeMetadata() {
|
|
94
|
+
return {
|
|
95
|
+
source: cachedStoreSource,
|
|
96
|
+
lastLoadedAt: lastRouteNodeTimestamp
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Export route events modal (primary export - everything you need!)
|
|
4
|
+
export { RouteEventsModalWithTabs } from "./components/RouteEventsModalWithTabs";
|
|
5
|
+
// Export preset configuration (easiest way to add to FloatingDevTools!)
|
|
6
|
+
export { routeEventsToolPreset, createRouteEventsTool } from "./preset";
|
|
7
|
+
|
|
8
|
+
// Export RouteTracker component - place inside your navigation tree for route tracking
|
|
9
|
+
export { RouteTracker } from "./RouteTracker";
|
|
10
|
+
|
|
11
|
+
// Export individual components for advanced usage
|
|
12
|
+
export { RoutesSitemap } from "./components/RoutesSitemap";
|
|
13
|
+
export { NavigationStack } from "./components/NavigationStack";
|
|
14
|
+
// Export advanced/optional utilities
|
|
15
|
+
// Note: Most users won't need these - the modal handles everything automatically
|
|
16
|
+
export { RouteObserver, routeObserver } from "./RouteObserver";
|
|
17
|
+
export { useRouteObserver } from "./useRouteObserver";
|
|
18
|
+
// Export route parser utilities for advanced use cases
|
|
19
|
+
export { RouteParser } from "./RouteParser";
|
|
20
|
+
// Export route sitemap hooks for advanced use cases
|
|
21
|
+
export { useRouteSitemap, useRoute, useParentRoutes } from "./useRouteSitemap";
|
|
22
|
+
// Export navigation stack utilities for advanced use cases
|
|
23
|
+
export { useNavigationStack } from "./useNavigationStack";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-configured route events tool for FloatingDevTools
|
|
5
|
+
*
|
|
6
|
+
* This preset provides a zero-config way to add route tracking to your dev tools.
|
|
7
|
+
* Just import and spread it into your apps array!
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { routeEventsToolPreset } from '@buoy-gg/route-events';
|
|
12
|
+
*
|
|
13
|
+
* const installedApps = [
|
|
14
|
+
* routeEventsToolPreset, // That's it!
|
|
15
|
+
* // ...other tools
|
|
16
|
+
* ];
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { RoutesIcon } from "@buoy-gg/floating-tools-core";
|
|
21
|
+
import { RouteEventsModalWithTabs } from "./components/RouteEventsModalWithTabs";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pre-configured route events tool for FloatingDevTools.
|
|
25
|
+
* Includes:
|
|
26
|
+
* - Route sitemap browser
|
|
27
|
+
* - Event timeline with filtering
|
|
28
|
+
* - Navigation stack visualization
|
|
29
|
+
* - Automatic route tracking (no setup needed)
|
|
30
|
+
*/
|
|
31
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
32
|
+
export const routeEventsToolPreset = {
|
|
33
|
+
id: "route-events",
|
|
34
|
+
name: "ROUTES",
|
|
35
|
+
description: "Route tracking & navigation inspector",
|
|
36
|
+
slot: "both",
|
|
37
|
+
icon: ({
|
|
38
|
+
size
|
|
39
|
+
}) => /*#__PURE__*/_jsx(RoutesIcon, {
|
|
40
|
+
size: size
|
|
41
|
+
}),
|
|
42
|
+
component: RouteEventsModalWithTabs,
|
|
43
|
+
props: {
|
|
44
|
+
enableSharedModalDimensions: false
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a custom route events tool configuration.
|
|
50
|
+
* Use this if you want to override default settings.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import { createRouteEventsTool } from '@buoy-gg/route-events';
|
|
55
|
+
*
|
|
56
|
+
* const myRouteTool = createRouteEventsTool({
|
|
57
|
+
* name: "MY ROUTES",
|
|
58
|
+
* color: "#a78bfa",
|
|
59
|
+
* enableSharedModalDimensions: true,
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function createRouteEventsTool(options) {
|
|
64
|
+
return {
|
|
65
|
+
id: options?.id || "route-events",
|
|
66
|
+
name: options?.name || "ROUTES",
|
|
67
|
+
description: options?.description || "Route tracking & navigation inspector",
|
|
68
|
+
slot: "both",
|
|
69
|
+
icon: ({
|
|
70
|
+
size
|
|
71
|
+
}) => /*#__PURE__*/_jsx(RoutesIcon, {
|
|
72
|
+
size: size
|
|
73
|
+
}),
|
|
74
|
+
component: RouteEventsModalWithTabs,
|
|
75
|
+
props: {
|
|
76
|
+
enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useNavigationStack - Hook to access current navigation stack state
|
|
5
|
+
*
|
|
6
|
+
* Provides real-time access to the navigation stack from Expo Router,
|
|
7
|
+
* showing what screens are currently mounted in memory and which is visible.
|
|
8
|
+
*
|
|
9
|
+
* Data source: @react-navigation/native
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useEffect, useMemo } from "react";
|
|
13
|
+
import { useNavigation, useNavigationState } from "@react-navigation/native";
|
|
14
|
+
import { router } from "expo-router";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Type Definitions
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Main Hook
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recursively collect all routes from the navigation state tree
|
|
26
|
+
*/
|
|
27
|
+
function collectRoutesFromState(state, depth = 0, parentPath = "") {
|
|
28
|
+
if (!state || !state.routes) return [];
|
|
29
|
+
const routes = [];
|
|
30
|
+
for (let i = 0; i < state.routes.length; i++) {
|
|
31
|
+
const route = state.routes[i];
|
|
32
|
+
const isFocused = i === state.index;
|
|
33
|
+
|
|
34
|
+
// Build the path
|
|
35
|
+
let pathname = route.name;
|
|
36
|
+
if (route.name === "index") {
|
|
37
|
+
pathname = "/";
|
|
38
|
+
} else if (!pathname.startsWith("/")) {
|
|
39
|
+
pathname = `/${pathname}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add parent path
|
|
43
|
+
if (parentPath && parentPath !== "/") {
|
|
44
|
+
pathname = `${parentPath}${pathname}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add params to pathname for dynamic routes
|
|
48
|
+
if (route.params) {
|
|
49
|
+
const paramEntries = Object.entries(route.params);
|
|
50
|
+
if (paramEntries.length > 0) {
|
|
51
|
+
pathname = pathname.replace(/\[([^\]]+)\]/g, (match, param) => {
|
|
52
|
+
return route.params[param] || match;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
routes.push({
|
|
57
|
+
key: route.key,
|
|
58
|
+
name: route.name,
|
|
59
|
+
path: pathname,
|
|
60
|
+
params: route.params || {},
|
|
61
|
+
state: route.state
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// If this route has nested state and is focused, recurse
|
|
65
|
+
if (isFocused && route.state) {
|
|
66
|
+
const nestedRoutes = collectRoutesFromState(route.state, depth + 1, pathname);
|
|
67
|
+
routes.push(...nestedRoutes);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return routes;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Access the current navigation stack from Expo Router
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* const { stack, focusedRoute, goBack, popToTop } = useNavigationStack();
|
|
79
|
+
*
|
|
80
|
+
* // Display stack
|
|
81
|
+
* stack.map(item => (
|
|
82
|
+
* <View key={item.key}>
|
|
83
|
+
* <Text>{item.pathname}</Text>
|
|
84
|
+
* {item.isFocused && <Text>VISIBLE</Text>}
|
|
85
|
+
* </View>
|
|
86
|
+
* ));
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function useNavigationStack() {
|
|
90
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
91
|
+
const [error, setError] = useState(null);
|
|
92
|
+
const navigation = useNavigation();
|
|
93
|
+
|
|
94
|
+
// Subscribe to navigation state changes - this will cause re-renders when state updates
|
|
95
|
+
const navigationState = useNavigationState(state => state);
|
|
96
|
+
|
|
97
|
+
// Mark as loaded once we have navigation
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
setIsLoaded(true);
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
// Transform navigation state into display items
|
|
103
|
+
const stack = useMemo(() => {
|
|
104
|
+
if (!navigationState) return [];
|
|
105
|
+
try {
|
|
106
|
+
const routes = collectRoutesFromState(navigationState);
|
|
107
|
+
|
|
108
|
+
// Filter out internal Expo Router routes (layouts, __root, etc.)
|
|
109
|
+
const filteredRoutes = routes.filter(route => {
|
|
110
|
+
// Remove __root and other internal routes
|
|
111
|
+
if (route.name.startsWith("__")) return false;
|
|
112
|
+
if (route.name.includes("_layout")) return false;
|
|
113
|
+
if (route.name.startsWith("+not-found")) return false;
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// If we filtered everything out, just keep the last route
|
|
118
|
+
const finalRoutes = filteredRoutes.length > 0 ? filteredRoutes : routes.slice(-1);
|
|
119
|
+
|
|
120
|
+
// The last route in the collected list is the focused one
|
|
121
|
+
return finalRoutes.map((route, index) => {
|
|
122
|
+
// Clean up pathname by removing /__root prefix
|
|
123
|
+
let cleanPath = route.path || `/${route.name}`;
|
|
124
|
+
cleanPath = cleanPath.replace(/^\/__root/, "");
|
|
125
|
+
// Ensure we always have at least a /
|
|
126
|
+
if (!cleanPath || cleanPath === "") {
|
|
127
|
+
cleanPath = "/";
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
key: route.key,
|
|
131
|
+
name: route.name,
|
|
132
|
+
pathname: cleanPath,
|
|
133
|
+
params: route.params || {},
|
|
134
|
+
isFocused: index === finalRoutes.length - 1,
|
|
135
|
+
index,
|
|
136
|
+
canPop: index > 0
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error("Error transforming navigation state:", err);
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}, [navigationState]);
|
|
144
|
+
|
|
145
|
+
// Get focused route
|
|
146
|
+
const focusedRoute = useMemo(() => {
|
|
147
|
+
return stack.find(item => item.isFocused) || null;
|
|
148
|
+
}, [stack]);
|
|
149
|
+
|
|
150
|
+
// Helper properties
|
|
151
|
+
const stackDepth = stack.length;
|
|
152
|
+
const isAtRoot = stackDepth <= 1;
|
|
153
|
+
|
|
154
|
+
// Manual refresh (no-op since we're using React Navigation's state)
|
|
155
|
+
const refresh = () => {
|
|
156
|
+
// Navigation state updates automatically via useNavigationState
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Navigation actions
|
|
160
|
+
const navigateToIndex = index => {
|
|
161
|
+
if (index >= stack.length || index < 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const targetRoute = stack[index];
|
|
165
|
+
if (!targetRoute) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Use router directly from expo-router
|
|
170
|
+
if (!router) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
router.navigate(targetRoute.pathname);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error("Failed to navigate:", err);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const popToIndex = index => {
|
|
180
|
+
if (index >= stack.length || index < 0) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const currentIndex = stack.length - 1;
|
|
184
|
+
const popCount = currentIndex - index;
|
|
185
|
+
if (popCount <= 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Use router directly from expo-router
|
|
190
|
+
if (!router) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
// Pop multiple times
|
|
195
|
+
for (let i = 0; i < popCount; i++) {
|
|
196
|
+
router.back();
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error("Failed to pop:", err);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const goBack = () => {
|
|
203
|
+
if (isAtRoot) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Use router directly from expo-router
|
|
208
|
+
if (!router) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
router.back();
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error("Failed to go back:", err);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const popToTop = () => {
|
|
218
|
+
if (isAtRoot) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Pop to index 0 (root)
|
|
223
|
+
popToIndex(0);
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
stack,
|
|
227
|
+
focusedRoute,
|
|
228
|
+
stackDepth,
|
|
229
|
+
isAtRoot,
|
|
230
|
+
isLoaded,
|
|
231
|
+
error,
|
|
232
|
+
refresh,
|
|
233
|
+
navigateToIndex,
|
|
234
|
+
popToIndex,
|
|
235
|
+
goBack,
|
|
236
|
+
popToTop
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useRouteObserver - React hook for observing route changes
|
|
5
|
+
*
|
|
6
|
+
* Uses public Expo Router hooks to track route changes and emits them
|
|
7
|
+
* to the global RouteObserver singleton.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useEffect, useRef } from "react";
|
|
11
|
+
import { usePathname, useSegments, useGlobalSearchParams } from "expo-router";
|
|
12
|
+
import { routeObserver } from "./RouteObserver";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook to observe route changes in Expo Router
|
|
16
|
+
* Automatically emits events to the global RouteObserver
|
|
17
|
+
*
|
|
18
|
+
* @param callback - Optional function to call on route changes (in addition to the observer)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* // Just track routes (no custom callback)
|
|
23
|
+
* useRouteObserver();
|
|
24
|
+
*
|
|
25
|
+
* // Track routes with custom callback
|
|
26
|
+
* useRouteObserver((event) => {
|
|
27
|
+
* // Handle route change
|
|
28
|
+
* analytics.trackPageView(event.pathname);
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function useRouteObserver(callback) {
|
|
33
|
+
const pathname = usePathname();
|
|
34
|
+
const segments = useSegments();
|
|
35
|
+
const params = useGlobalSearchParams();
|
|
36
|
+
const callbackRef = useRef(callback);
|
|
37
|
+
const previousPathnameRef = useRef(undefined);
|
|
38
|
+
const previousTimestampRef = useRef(undefined);
|
|
39
|
+
|
|
40
|
+
// Update ref when callback changes
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
callbackRef.current = callback;
|
|
43
|
+
}, [callback]);
|
|
44
|
+
|
|
45
|
+
// Trigger observer and callback whenever route changes
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const timeSincePrevious = previousTimestampRef.current ? now - previousTimestampRef.current : undefined;
|
|
49
|
+
const event = {
|
|
50
|
+
pathname,
|
|
51
|
+
params: params,
|
|
52
|
+
segments: segments,
|
|
53
|
+
timestamp: now,
|
|
54
|
+
previousPathname: previousPathnameRef.current,
|
|
55
|
+
timeSincePrevious
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Update refs for next navigation
|
|
59
|
+
previousPathnameRef.current = pathname;
|
|
60
|
+
previousTimestampRef.current = now;
|
|
61
|
+
|
|
62
|
+
// Emit to the global observer (this notifies the modal)
|
|
63
|
+
routeObserver.emit(event);
|
|
64
|
+
|
|
65
|
+
// Also call the custom callback if provided
|
|
66
|
+
if (callbackRef.current) {
|
|
67
|
+
callbackRef.current(event);
|
|
68
|
+
}
|
|
69
|
+
}, [pathname, segments, params]);
|
|
70
|
+
}
|