@teardown/dev-client 2.0.44
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/android/build.gradle.kts +34 -0
- package/android/react-native.config.js +10 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/com/teardown/devclient/DevSettingsModule.kt +130 -0
- package/android/src/main/java/com/teardown/devclient/ShakeDetectorModule.kt +118 -0
- package/android/src/main/java/com/teardown/devclient/TeardownDevClientPackage.kt +23 -0
- package/ios/TeardownDevClient/DevSettingsModule.swift +135 -0
- package/ios/TeardownDevClient/ShakeDetector.swift +102 -0
- package/ios/TeardownDevClient/TeardownDevClient.h +14 -0
- package/ios/TeardownDevClient/TeardownDevClient.mm +42 -0
- package/ios/TeardownDevClient.podspec +23 -0
- package/package.json +56 -0
- package/src/components/dev-menu/dev-menu.tsx +254 -0
- package/src/components/dev-menu/index.ts +5 -0
- package/src/components/error-overlay/error-overlay.tsx +256 -0
- package/src/components/error-overlay/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/components/splash-screen/index.ts +5 -0
- package/src/components/splash-screen/splash-screen.tsx +99 -0
- package/src/dev-client-provider.tsx +204 -0
- package/src/hooks/index.ts +24 -0
- package/src/hooks/use-bundler-status.ts +139 -0
- package/src/hooks/use-dev-menu.ts +306 -0
- package/src/hooks/use-splash-screen.ts +177 -0
- package/src/index.ts +77 -0
- package/src/native/dev-settings.ts +132 -0
- package/src/native/index.ts +16 -0
- package/src/native/shake-detector.ts +105 -0
- package/src/types.ts +235 -0
- package/src/utils/bundler-url.ts +103 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/platform.ts +64 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splash screen control hook
|
|
3
|
+
*
|
|
4
|
+
* Provides state and control methods for the splash screen.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
import type { SplashScreenApi, SplashScreenConfig } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default splash screen configuration
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG: Required<SplashScreenConfig> = {
|
|
14
|
+
backgroundColor: "#ffffff",
|
|
15
|
+
logo: undefined as unknown as Required<SplashScreenConfig>["logo"],
|
|
16
|
+
resizeMode: "contain",
|
|
17
|
+
minDisplayTime: 500,
|
|
18
|
+
fadeOutDuration: 300,
|
|
19
|
+
autoHide: true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for useSplashScreen hook
|
|
24
|
+
*/
|
|
25
|
+
export interface UseSplashScreenOptions {
|
|
26
|
+
/** Splash screen configuration */
|
|
27
|
+
config?: SplashScreenConfig;
|
|
28
|
+
|
|
29
|
+
/** Callback when splash screen is ready to hide */
|
|
30
|
+
onReady?: () => void;
|
|
31
|
+
|
|
32
|
+
/** Callback when splash screen has hidden */
|
|
33
|
+
onHide?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hook return type
|
|
38
|
+
*/
|
|
39
|
+
export interface UseSplashScreenReturn extends SplashScreenApi {
|
|
40
|
+
/** Merged configuration */
|
|
41
|
+
config: Required<SplashScreenConfig>;
|
|
42
|
+
|
|
43
|
+
/** Whether fade animation is in progress */
|
|
44
|
+
isFading: boolean;
|
|
45
|
+
|
|
46
|
+
/** Opacity value for animation (0-1) */
|
|
47
|
+
opacity: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Hook for managing splash screen state
|
|
52
|
+
*
|
|
53
|
+
* @param options - Configuration options
|
|
54
|
+
* @returns Splash screen state and control methods
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* function App() {
|
|
59
|
+
* const splash = useSplashScreen({
|
|
60
|
+
* config: { minDisplayTime: 1000 },
|
|
61
|
+
* onReady: () => console.log('App ready'),
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* useEffect(() => {
|
|
65
|
+
* // App initialization logic
|
|
66
|
+
* initializeApp().then(() => {
|
|
67
|
+
* splash.hide();
|
|
68
|
+
* });
|
|
69
|
+
* }, []);
|
|
70
|
+
*
|
|
71
|
+
* return splash.isVisible ? <SplashScreen {...splash} /> : <MainApp />;
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function useSplashScreen(options: UseSplashScreenOptions = {}): UseSplashScreenReturn {
|
|
76
|
+
const { config: userConfig, onReady, onHide } = options;
|
|
77
|
+
|
|
78
|
+
// Merge config with defaults
|
|
79
|
+
const config: Required<SplashScreenConfig> = {
|
|
80
|
+
...DEFAULT_CONFIG,
|
|
81
|
+
...userConfig,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
85
|
+
const [isFading, setIsFading] = useState(false);
|
|
86
|
+
const [opacity, setOpacity] = useState(1);
|
|
87
|
+
|
|
88
|
+
const showTimeRef = useRef(Date.now());
|
|
89
|
+
const fadeIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
90
|
+
const hasCalledOnReady = useRef(false);
|
|
91
|
+
|
|
92
|
+
// Cleanup function
|
|
93
|
+
const cleanup = useCallback(() => {
|
|
94
|
+
if (fadeIntervalRef.current) {
|
|
95
|
+
clearInterval(fadeIntervalRef.current);
|
|
96
|
+
fadeIntervalRef.current = null;
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
// Hide splash screen with optional fade
|
|
101
|
+
const hide = useCallback(
|
|
102
|
+
async (hideOptions?: { fade?: boolean }): Promise<void> => {
|
|
103
|
+
const shouldFade = hideOptions?.fade ?? true;
|
|
104
|
+
const elapsed = Date.now() - showTimeRef.current;
|
|
105
|
+
const remaining = Math.max(0, config.minDisplayTime - elapsed);
|
|
106
|
+
|
|
107
|
+
// Wait for minimum display time
|
|
108
|
+
if (remaining > 0) {
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!shouldFade || config.fadeOutDuration <= 0) {
|
|
113
|
+
// Instant hide
|
|
114
|
+
setIsVisible(false);
|
|
115
|
+
onHide?.();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fade animation
|
|
120
|
+
setIsFading(true);
|
|
121
|
+
const steps = 20;
|
|
122
|
+
const stepDuration = config.fadeOutDuration / steps;
|
|
123
|
+
const opacityStep = 1 / steps;
|
|
124
|
+
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
let currentStep = 0;
|
|
127
|
+
fadeIntervalRef.current = setInterval(() => {
|
|
128
|
+
currentStep++;
|
|
129
|
+
setOpacity(1 - currentStep * opacityStep);
|
|
130
|
+
|
|
131
|
+
if (currentStep >= steps) {
|
|
132
|
+
cleanup();
|
|
133
|
+
setIsVisible(false);
|
|
134
|
+
setIsFading(false);
|
|
135
|
+
onHide?.();
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
}, stepDuration);
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
[config.minDisplayTime, config.fadeOutDuration, onHide, cleanup]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Show splash screen
|
|
145
|
+
const show = useCallback(async (): Promise<void> => {
|
|
146
|
+
showTimeRef.current = Date.now();
|
|
147
|
+
setOpacity(1);
|
|
148
|
+
setIsFading(false);
|
|
149
|
+
setIsVisible(true);
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
// Auto-hide when component mounts (if enabled)
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!hasCalledOnReady.current) {
|
|
155
|
+
hasCalledOnReady.current = true;
|
|
156
|
+
onReady?.();
|
|
157
|
+
|
|
158
|
+
if (config.autoHide) {
|
|
159
|
+
hide();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}, [config.autoHide, hide, onReady]);
|
|
163
|
+
|
|
164
|
+
// Cleanup on unmount
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
return cleanup;
|
|
167
|
+
}, [cleanup]);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
isVisible,
|
|
171
|
+
isFading,
|
|
172
|
+
opacity,
|
|
173
|
+
config,
|
|
174
|
+
hide,
|
|
175
|
+
show,
|
|
176
|
+
};
|
|
177
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @teardown/dev-client
|
|
3
|
+
*
|
|
4
|
+
* React Native development client for Teardown applications.
|
|
5
|
+
* Provides dev menu, splash screen, and error overlay functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Components
|
|
9
|
+
export { DevMenu, type DevMenuProps } from "./components/dev-menu";
|
|
10
|
+
export { ErrorOverlay, type ErrorOverlayProps } from "./components/error-overlay";
|
|
11
|
+
export { SplashScreen, type SplashScreenProps } from "./components/splash-screen";
|
|
12
|
+
// Provider
|
|
13
|
+
export {
|
|
14
|
+
DevClientProvider,
|
|
15
|
+
type DevClientProviderProps,
|
|
16
|
+
useDevClient,
|
|
17
|
+
useDevClientMenu,
|
|
18
|
+
useDevClientSplash,
|
|
19
|
+
} from "./dev-client-provider";
|
|
20
|
+
// Hooks
|
|
21
|
+
export {
|
|
22
|
+
createBundlerStatus,
|
|
23
|
+
type UseBundlerStatusOptions,
|
|
24
|
+
useBundlerStatus,
|
|
25
|
+
} from "./hooks/use-bundler-status";
|
|
26
|
+
export {
|
|
27
|
+
createInitialDevMenuState,
|
|
28
|
+
getDefaultMenuItems,
|
|
29
|
+
type UseDevMenuOptions,
|
|
30
|
+
type UseDevMenuReturn,
|
|
31
|
+
useDevMenu,
|
|
32
|
+
} from "./hooks/use-dev-menu";
|
|
33
|
+
export {
|
|
34
|
+
type UseSplashScreenOptions,
|
|
35
|
+
type UseSplashScreenReturn,
|
|
36
|
+
useSplashScreen,
|
|
37
|
+
} from "./hooks/use-splash-screen";
|
|
38
|
+
// Native modules (for advanced use)
|
|
39
|
+
export {
|
|
40
|
+
getDevSettings,
|
|
41
|
+
isNativeModuleAvailable,
|
|
42
|
+
openDebugger,
|
|
43
|
+
openDevMenu,
|
|
44
|
+
reload,
|
|
45
|
+
setFastRefreshEnabled,
|
|
46
|
+
setHotLoadingEnabled,
|
|
47
|
+
toggleElementInspector,
|
|
48
|
+
} from "./native/dev-settings";
|
|
49
|
+
export { ShakeDetector } from "./native/shake-detector";
|
|
50
|
+
// Types
|
|
51
|
+
export type {
|
|
52
|
+
BundlerStatus,
|
|
53
|
+
DevClientConfig,
|
|
54
|
+
DevClientContextValue,
|
|
55
|
+
DevMenuActions,
|
|
56
|
+
DevMenuItem,
|
|
57
|
+
DevMenuState,
|
|
58
|
+
DevSettings,
|
|
59
|
+
GetBundlerUrlOptions,
|
|
60
|
+
SplashScreenApi,
|
|
61
|
+
SplashScreenConfig,
|
|
62
|
+
} from "./types";
|
|
63
|
+
// Utilities
|
|
64
|
+
export {
|
|
65
|
+
DEFAULT_BUNDLER_PORT,
|
|
66
|
+
getBundlerUrl,
|
|
67
|
+
getDefaultBundlerUrl,
|
|
68
|
+
isValidBundlerUrl,
|
|
69
|
+
} from "./utils/bundler-url";
|
|
70
|
+
export {
|
|
71
|
+
currentPlatform,
|
|
72
|
+
isAndroid,
|
|
73
|
+
isDev,
|
|
74
|
+
isEmulator,
|
|
75
|
+
isIOS,
|
|
76
|
+
platformSelect,
|
|
77
|
+
} from "./utils/platform";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native dev settings module wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified interface for interacting with React Native's
|
|
5
|
+
* dev settings across iOS and Android.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NativeModules } from "react-native";
|
|
9
|
+
import type { DevSettings, DevSettingsNativeModule } from "../types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Teardown native module - will be available when native modules are built
|
|
13
|
+
*/
|
|
14
|
+
const TeardownDevSettings = NativeModules.TeardownDevSettings as DevSettingsNativeModule | undefined;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* React Native's built-in DevSettings module
|
|
18
|
+
*/
|
|
19
|
+
const RNDevSettings = NativeModules.DevSettings as
|
|
20
|
+
| {
|
|
21
|
+
reload?: () => void;
|
|
22
|
+
setHotLoadingEnabled?: (enabled: boolean) => void;
|
|
23
|
+
setProfilingEnabled?: (enabled: boolean) => void;
|
|
24
|
+
}
|
|
25
|
+
| undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* React Native's DevMenu module
|
|
29
|
+
*/
|
|
30
|
+
const RNDevMenu = NativeModules.DevMenu as
|
|
31
|
+
| {
|
|
32
|
+
show?: () => void;
|
|
33
|
+
}
|
|
34
|
+
| undefined;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reload the JavaScript bundle
|
|
38
|
+
*/
|
|
39
|
+
export function reload(): void {
|
|
40
|
+
if (TeardownDevSettings?.reload) {
|
|
41
|
+
TeardownDevSettings.reload();
|
|
42
|
+
} else if (RNDevSettings?.reload) {
|
|
43
|
+
RNDevSettings.reload();
|
|
44
|
+
} else {
|
|
45
|
+
console.warn("[DevClient] Reload not available - native module not found");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Open the debugger
|
|
51
|
+
*/
|
|
52
|
+
export function openDebugger(): void {
|
|
53
|
+
if (TeardownDevSettings?.openDebugger) {
|
|
54
|
+
TeardownDevSettings.openDebugger();
|
|
55
|
+
} else {
|
|
56
|
+
console.warn("[DevClient] OpenDebugger not available - native module not found");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Toggle element inspector overlay
|
|
62
|
+
*/
|
|
63
|
+
export function toggleElementInspector(): void {
|
|
64
|
+
if (TeardownDevSettings?.toggleElementInspector) {
|
|
65
|
+
TeardownDevSettings.toggleElementInspector();
|
|
66
|
+
} else {
|
|
67
|
+
console.warn("[DevClient] ToggleElementInspector not available - native module not found");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set hot loading enabled state
|
|
73
|
+
*/
|
|
74
|
+
export function setHotLoadingEnabled(enabled: boolean): void {
|
|
75
|
+
if (TeardownDevSettings?.setHotLoadingEnabled) {
|
|
76
|
+
TeardownDevSettings.setHotLoadingEnabled(enabled);
|
|
77
|
+
} else if (RNDevSettings?.setHotLoadingEnabled) {
|
|
78
|
+
RNDevSettings.setHotLoadingEnabled(enabled);
|
|
79
|
+
} else {
|
|
80
|
+
console.warn("[DevClient] SetHotLoadingEnabled not available - native module not found");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Set fast refresh enabled state
|
|
86
|
+
*/
|
|
87
|
+
export function setFastRefreshEnabled(enabled: boolean): void {
|
|
88
|
+
if (TeardownDevSettings?.setFastRefreshEnabled) {
|
|
89
|
+
TeardownDevSettings.setFastRefreshEnabled(enabled);
|
|
90
|
+
} else {
|
|
91
|
+
// Fast refresh uses the same setting as hot loading in RN
|
|
92
|
+
setHotLoadingEnabled(enabled);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get current dev settings
|
|
98
|
+
*/
|
|
99
|
+
export async function getDevSettings(): Promise<DevSettings> {
|
|
100
|
+
if (TeardownDevSettings?.getDevSettings) {
|
|
101
|
+
return TeardownDevSettings.getDevSettings();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Return default values if native module not available
|
|
105
|
+
return {
|
|
106
|
+
isDebuggingRemotely: false,
|
|
107
|
+
isElementInspectorShown: false,
|
|
108
|
+
isHotLoadingEnabled: true,
|
|
109
|
+
isFastRefreshEnabled: true,
|
|
110
|
+
isPerfMonitorShown: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Open native dev menu
|
|
116
|
+
*/
|
|
117
|
+
export function openDevMenu(): void {
|
|
118
|
+
if (TeardownDevSettings?.openDevMenu) {
|
|
119
|
+
TeardownDevSettings.openDevMenu();
|
|
120
|
+
} else if (RNDevMenu?.show) {
|
|
121
|
+
RNDevMenu.show();
|
|
122
|
+
} else {
|
|
123
|
+
console.warn("[DevClient] OpenDevMenu not available - native module not found");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if native module is available
|
|
129
|
+
*/
|
|
130
|
+
export function isNativeModuleAvailable(): boolean {
|
|
131
|
+
return TeardownDevSettings !== undefined;
|
|
132
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
getDevSettings,
|
|
7
|
+
isNativeModuleAvailable,
|
|
8
|
+
openDebugger,
|
|
9
|
+
openDevMenu,
|
|
10
|
+
reload,
|
|
11
|
+
setFastRefreshEnabled,
|
|
12
|
+
setHotLoadingEnabled,
|
|
13
|
+
toggleElementInspector,
|
|
14
|
+
} from "./dev-settings";
|
|
15
|
+
|
|
16
|
+
export { ShakeDetector } from "./shake-detector";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shake gesture detection
|
|
3
|
+
*
|
|
4
|
+
* Provides shake-to-open-dev-menu functionality using
|
|
5
|
+
* React Native's DeviceEventEmitter or native module.
|
|
6
|
+
* Uses lazy require so tests can run when react-native is not fully available.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import EventEmitter from "eventemitter3";
|
|
10
|
+
|
|
11
|
+
function getRN(): {
|
|
12
|
+
DeviceEventEmitter?: { addListener: (a: string, b: () => void) => { remove: () => void } };
|
|
13
|
+
NativeModules?: { TeardownShakeDetector?: { startListening?: () => void; stopListening?: () => void } };
|
|
14
|
+
} {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
+
return require("react-native") as ReturnType<typeof getRN>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Native shake detector module (lazy)
|
|
21
|
+
*/
|
|
22
|
+
function getTeardownShakeDetector() {
|
|
23
|
+
return getRN().NativeModules?.TeardownShakeDetector;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Event types emitted by shake detector
|
|
28
|
+
*/
|
|
29
|
+
export interface ShakeDetectorEvents {
|
|
30
|
+
shake: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ShakeDetector class for managing shake gesture detection
|
|
35
|
+
*/
|
|
36
|
+
class ShakeDetectorImpl extends EventEmitter<ShakeDetectorEvents> {
|
|
37
|
+
private isListening = false;
|
|
38
|
+
private subscription: { remove: () => void } | null = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Start listening for shake gestures
|
|
42
|
+
*/
|
|
43
|
+
start(): void {
|
|
44
|
+
if (this.isListening) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rn = getRN();
|
|
49
|
+
const DeviceEventEmitter = rn.DeviceEventEmitter;
|
|
50
|
+
if (!DeviceEventEmitter) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.isListening = true;
|
|
55
|
+
const TeardownShakeDetector = getTeardownShakeDetector();
|
|
56
|
+
|
|
57
|
+
// Try native module first
|
|
58
|
+
if (TeardownShakeDetector?.startListening) {
|
|
59
|
+
TeardownShakeDetector.startListening();
|
|
60
|
+
this.subscription = DeviceEventEmitter.addListener("TeardownShakeEvent", () => {
|
|
61
|
+
this.emit("shake");
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
// Fallback: Listen for RN's shake event (requires Debug mode)
|
|
65
|
+
this.subscription = DeviceEventEmitter.addListener("RCTDevMenuShown", () => {
|
|
66
|
+
// This fires when the native dev menu is shown via shake
|
|
67
|
+
// We can use this as a proxy for shake detection
|
|
68
|
+
this.emit("shake");
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Stop listening for shake gestures
|
|
75
|
+
*/
|
|
76
|
+
stop(): void {
|
|
77
|
+
if (!this.isListening) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.isListening = false;
|
|
82
|
+
const TeardownShakeDetector = getTeardownShakeDetector();
|
|
83
|
+
|
|
84
|
+
if (TeardownShakeDetector?.stopListening) {
|
|
85
|
+
TeardownShakeDetector.stopListening();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.subscription) {
|
|
89
|
+
this.subscription.remove();
|
|
90
|
+
this.subscription = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if currently listening
|
|
96
|
+
*/
|
|
97
|
+
getIsListening(): boolean {
|
|
98
|
+
return this.isListening;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Singleton shake detector instance
|
|
104
|
+
*/
|
|
105
|
+
export const ShakeDetector = new ShakeDetectorImpl();
|