diff --git a/configs/test-utils/src/__mocks__/@monkvision/common.tsx b/configs/test-utils/src/__mocks__/@monkvision/common.tsx
index 11ad3eb7a..f20501196 100644
--- a/configs/test-utils/src/__mocks__/@monkvision/common.tsx
+++ b/configs/test-utils/src/__mocks__/@monkvision/common.tsx
@@ -118,5 +118,5 @@ export = {
useAsyncInterval: jest.fn(),
useSearchParams: jest.fn(() => ({ get: jest.fn(() => null) })),
useObjectMemo: jest.fn((obj) => obj),
- usePreventExit: jest.fn(),
+ usePreventExit: jest.fn(() => ({ allowRedirect: jest.fn() })),
};
diff --git a/packages/common/README/HOOKS.md b/packages/common/README/HOOKS.md
index 11860efc4..847e02377 100644
--- a/packages/common/README/HOOKS.md
+++ b/packages/common/README/HOOKS.md
@@ -137,12 +137,16 @@ function TestComponent() {
Custom hook used to get a translation function tObj that translates TranslationObjects.
### usePreventExit
-```tsx
+TODO: need to add
+```ts
import { usePreventExit } from '@monkvision/common';
-function TestComponent() {
- usePreventExit();
- return <>...>;
+function MyComponent() {
+ const { forceOut } = usePreventExit(true);// commonly it should be a expression.
+ function anyEvent(){
+ forceOut();
+ // Possible Navigation
+ }
}
```
This hook is used to prevent the user from leaving the page by displaying a confirmation dialog when the user tries to
diff --git a/packages/common/src/PreventExit/hooks.ts b/packages/common/src/PreventExit/hooks.ts
new file mode 100644
index 000000000..dd48f2d1f
--- /dev/null
+++ b/packages/common/src/PreventExit/hooks.ts
@@ -0,0 +1,25 @@
+import { useEffect, useMemo } from 'react';
+import { createPreventExitListener } from './store';
+
+/**
+ * Custom hook that allows you
+ * to access the PreventExit Context methods inside a component.
+ *
+ * Note : If this hook is called inside a component
+ * that is not a child of a PreventExit component,
+ * it will throw an error.
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { forceOut } = usePreventExit(true);// commonly it should be a expression.
+ * return
forceOut()}>My Component
;
+ * }
+ * ```
+ */
+export function usePreventExit(preventExit: boolean) {
+ const { cleanup, setPreventExit, allowRedirect } = useMemo(createPreventExitListener, []);
+ useMemo(() => setPreventExit(preventExit), [preventExit]);
+ useEffect(() => cleanup, []);
+ return { allowRedirect };
+}
diff --git a/packages/common/src/PreventExit/index.ts b/packages/common/src/PreventExit/index.ts
new file mode 100644
index 000000000..4cc90d02b
--- /dev/null
+++ b/packages/common/src/PreventExit/index.ts
@@ -0,0 +1 @@
+export * from './hooks';
diff --git a/packages/common/src/PreventExit/store.ts b/packages/common/src/PreventExit/store.ts
new file mode 100644
index 000000000..c2592302f
--- /dev/null
+++ b/packages/common/src/PreventExit/store.ts
@@ -0,0 +1,49 @@
+const keys: Array = [];
+const allPreventExitState: Record = {};
+
+function checkNoMorePreventExit() {
+ if (keys.map((key) => allPreventExitState[key]).every((i) => i === false)) {
+ window.onbeforeunload = null;
+ return true;
+ }
+ return false;
+}
+function publish(id: symbol, preventExit: boolean) {
+ allPreventExitState[id] = preventExit;
+ if (!checkNoMorePreventExit())
+ window.onbeforeunload = (e) => {
+ e.preventDefault();
+ return 'Confirm Alert appears';
+ };
+}
+/**
+ * Creates a listener function that manages the preventExit state of a component.
+ */
+export function createPreventExitListener() {
+ const key = Symbol('PreventExitListener');
+ allPreventExitState[key] = true;
+ keys.push(key);
+ return {
+ /**
+ * To change the preventExit state of the component.
+ */
+ setPreventExit: (preventExit: boolean) => {
+ publish(key, preventExit);
+ },
+ /**
+ * Allows the user to leave the page without confirmation temporarily.
+ * This should be used when the developer wants to explicitly allow navigation.
+ */
+ allowRedirect: () => {
+ window.onbeforeunload = null;
+ },
+ /**
+ * Performs garbage collection by removing the preventExit state associated with the component.
+ * This should be used when the component is unmounted.
+ */
+ cleanup: () => {
+ delete allPreventExitState[key];
+ checkNoMorePreventExit();
+ },
+ };
+}
diff --git a/packages/common/src/hooks/index.ts b/packages/common/src/hooks/index.ts
index 6bb9aa318..7b2cabb19 100644
--- a/packages/common/src/hooks/index.ts
+++ b/packages/common/src/hooks/index.ts
@@ -10,4 +10,3 @@ export * from './useSearchParams';
export * from './useInterval';
export * from './useAsyncInterval';
export * from './useObjectMemo';
-export * from './usePreventExit';
diff --git a/packages/common/src/hooks/usePreventExit.ts b/packages/common/src/hooks/usePreventExit.ts
deleted file mode 100644
index 528a532a6..000000000
--- a/packages/common/src/hooks/usePreventExit.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useEffect } from 'react';
-
-/**
- * Hook that prevents the user from leaving the page without confirmation.
- *
- * @example
- * ```tsx
- * import { usePreventExit } from '@monkvision/common';
- *
- * function MyComponent() {
- * usePreventExit();
- * return My Component
;
- * }
- * ```
- */
-export function usePreventExit() {
- useEffect(() => {
- window.onbeforeunload = () => {
- return 'Are you sure you want to leave?';
- };
- return () => {
- window.onbeforeunload = null;
- };
- }, []);
-}
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index c93c4d6a0..cbc74b6ab 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -1,6 +1,7 @@
+export * from './PreventExit';
+export * from './apps';
+export * from './hooks';
export * from './i18n';
export * from './state';
export * from './theme';
export * from './utils';
-export * from './hooks';
-export * from './apps';
diff --git a/packages/common/test/PreventExit/hooks.test.ts b/packages/common/test/PreventExit/hooks.test.ts
new file mode 100644
index 000000000..7d7222282
--- /dev/null
+++ b/packages/common/test/PreventExit/hooks.test.ts
@@ -0,0 +1,46 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { usePreventExit } from '../../src/PreventExit/hooks';
+import { createPreventExitListener } from '../../src/PreventExit/store';
+
+jest.mock('../../src/PreventExit/store', () => ({
+ createPreventExitListener: jest.fn(() => ({
+ cleanup: jest.fn(),
+ setPreventExit: jest.fn(),
+ allowRedirect: jest.fn(),
+ })),
+}));
+describe('PreventExit hook usePreventExit', () => {
+ let spyCreatePreventExitListener: jest.SpyInstance;
+ beforeEach(() => {
+ spyCreatePreventExitListener = jest.spyOn(
+ require('../../src/PreventExit/store'),
+ 'createPreventExitListener',
+ );
+ });
+ afterEach(() => jest.clearAllMocks());
+
+ it('should clean up when unmount', () => {
+ const { unmount } = renderHook(() => usePreventExit(true));
+ unmount();
+ expect(spyCreatePreventExitListener.mock.results.at(-1)?.value.cleanup).toHaveBeenCalledTimes(
+ 1,
+ );
+ });
+
+ it('should set preventExit when value changes', async () => {
+ const { rerender } = renderHook((props) => usePreventExit(props), {
+ initialProps: true,
+ });
+ expect(
+ spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
+ ).toHaveBeenCalledTimes(1);
+ rerender(false);
+ expect(
+ spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
+ ).toHaveBeenCalledTimes(2);
+ rerender(false);
+ expect(
+ spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
+ ).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/common/test/PreventExit/store.test.ts b/packages/common/test/PreventExit/store.test.ts
new file mode 100644
index 000000000..725223c48
--- /dev/null
+++ b/packages/common/test/PreventExit/store.test.ts
@@ -0,0 +1,49 @@
+import { createPreventExitListener } from '../../src/PreventExit/store';
+
+describe('preventExitStore', () => {
+ let listener1: ReturnType;
+ let listener2: ReturnType;
+
+ beforeEach(() => {
+ listener1 = createPreventExitListener();
+ listener2 = createPreventExitListener();
+ });
+
+ afterEach(() => {
+ listener1.cleanup();
+ listener2.cleanup();
+ });
+
+ it('should prevent exit: listener 1', () => {
+ listener1.setPreventExit(true);
+ listener2.setPreventExit(true);
+ expect(window.onbeforeunload).not.toBe(null);
+ listener1.setPreventExit(true);
+ listener2.setPreventExit(false);
+ expect(window.onbeforeunload).not.toBe(null);
+ listener1.setPreventExit(false);
+ listener2.setPreventExit(true);
+ expect(window.onbeforeunload).not.toBe(null);
+ listener1.setPreventExit(false);
+ listener2.setPreventExit(false);
+ expect(window.onbeforeunload).toBe(null);
+ });
+
+ it('should allow redirect: listener 1', () => {
+ const preventExit = [true, false];
+ preventExit.forEach((i) => {
+ preventExit.forEach((j) => {
+ listener1.setPreventExit(i);
+ listener2.setPreventExit(j);
+ listener1.allowRedirect();
+ expect(window.onbeforeunload).toBe(null);
+ });
+ });
+ });
+
+ it('should allow redirect: lister 2', () => {
+ listener2.cleanup();
+ listener1.cleanup();
+ expect(window.onbeforeunload).toBe(null);
+ });
+});
diff --git a/packages/common/test/hooks/usePreventExit.test.ts b/packages/common/test/hooks/usePreventExit.test.ts
deleted file mode 100644
index 968b8163d..000000000
--- a/packages/common/test/hooks/usePreventExit.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { renderHook } from '@testing-library/react-hooks';
-import { usePreventExit } from '../../src';
-
-describe('usePreventExit hook', () => {
- it('should properly set and unset the onbeforeunload event handler', () => {
- const { unmount } = renderHook(usePreventExit);
- expect(window.onbeforeunload).toBeDefined();
- unmount();
- expect(window.onbeforeunload).toBeNull();
- });
-
- it('should prompt the user when trying to leave the page', () => {
- const { unmount } = renderHook(usePreventExit);
- const event = new Event('beforeunload', { cancelable: true });
- const returnValue = window.onbeforeunload?.(event);
- expect(returnValue).toEqual('Are you sure you want to leave?');
- unmount();
- });
-});
diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx
index 2c96b58a3..7c27fdc41 100644
--- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx
+++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx
@@ -4,6 +4,7 @@ import {
useI18nSync,
useLoadingState,
useObjectMemo,
+ usePreventExit,
useWindowDimensions,
} from '@monkvision/common';
import {
@@ -212,6 +213,7 @@ export function PhotoCapture({
}
setCurrentScreen(PhotoCaptureScreen.CAMERA);
};
+ const { allowRedirect } = usePreventExit(sightState.sightsTaken.length !== 0);
const handleGalleryValidate = () => {
startTasks()
.then(() => {
@@ -220,6 +222,7 @@ export function PhotoCapture({
captureCompleted: true,
sightSelected: 'inspection-completed',
});
+ allowRedirect();
onComplete?.();
})
.catch((err) => {
@@ -230,7 +233,6 @@ export function PhotoCapture({
const isViolatingEnforcedOrientation =
enforceOrientation &&
(enforceOrientation === DeviceOrientation.PORTRAIT) !== dimensions.isPortrait;
-
const hudProps: Omit = {
sights,
selectedSight: sightState.selectedSight,
diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx
index 0c9e9a0d0..41e722cf5 100644
--- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx
+++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx
@@ -51,12 +51,12 @@ jest.mock('../../src/PhotoCapture/hooks', () => ({
useTracking: jest.fn(),
}));
-import { act, render, waitFor } from '@testing-library/react';
import { Camera } from '@monkvision/camera-web';
-import { expectPropsOnChildMock } from '@monkvision/test-utils';
-import { useI18nSync, useLoadingState } from '@monkvision/common';
+import { useI18nSync, useLoadingState, usePreventExit } from '@monkvision/common';
import { BackdropDialog, InspectionGallery } from '@monkvision/common-ui-web';
import { useMonitoring } from '@monkvision/monitoring';
+import { expectPropsOnChildMock } from '@monkvision/test-utils';
+import { act, render, waitFor } from '@testing-library/react';
import { PhotoCapture, PhotoCaptureHUD, PhotoCaptureProps } from '../../src';
import {
useAdaptiveCameraConfig,
@@ -434,4 +434,11 @@ describe('PhotoCapture component', () => {
unmount();
});
+
+ it('should ask the user for confirmation before exit', () => {
+ const props = createProps();
+ const { unmount } = render();
+ expect(usePreventExit).toHaveBeenCalled();
+ unmount();
+ });
});