@granite-js/react-native 1.0.31 → 1.0.33

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/app/AppRoot.d.ts +1 -1
  3. package/dist/app/Granite.d.ts +8 -1
  4. package/dist/router/Router.d.ts +1 -1
  5. package/dist/router/components/CanGoBackGuard.d.ts +13 -5
  6. package/dist/router/components/useRouterBackHandler.d.ts +4 -1
  7. package/dist/use-back-event/BackEventContext.d.ts +15 -0
  8. package/dist/use-back-event/index.d.ts +1 -0
  9. package/dist/use-back-event/useBackEvent.d.ts +8 -9
  10. package/dist/use-back-event/useBackEventActionsContext.d.ts +1 -0
  11. package/dist/use-back-event/useBackEventContext.d.ts +2 -0
  12. package/dist/use-back-event/useBackHandler.d.ts +53 -0
  13. package/package.json +12 -11
  14. package/src/app/AppRoot.tsx +3 -0
  15. package/src/app/Granite.tsx +8 -1
  16. package/src/intersection-observer/IntersectionObserver.ts +3 -3
  17. package/src/router/Router.tsx +6 -2
  18. package/src/router/components/CanGoBackGuard.spec.tsx +85 -0
  19. package/src/router/components/CanGoBackGuard.tsx +37 -14
  20. package/src/router/components/useRouterBackHandler.spec.tsx +129 -0
  21. package/src/router/components/useRouterBackHandler.tsx +34 -13
  22. package/src/use-back-event/BackEventContext.ts +26 -0
  23. package/src/use-back-event/index.ts +1 -0
  24. package/src/use-back-event/useBackEvent.spec.tsx +107 -0
  25. package/src/use-back-event/useBackEvent.tsx +138 -48
  26. package/src/use-back-event/useBackEventActionsContext.ts +11 -0
  27. package/src/use-back-event/useBackEventContext.ts +28 -0
  28. package/src/use-back-event/useBackHandler.spec.tsx +113 -0
  29. package/src/use-back-event/useBackHandler.tsx +114 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @granite-js/react-native
2
2
 
3
+ ## 1.0.33
4
+
5
+ ### Patch Changes
6
+
7
+ - 39e17bb: Fix horizontal `IntersectionObserver` ratio calculation
8
+ - @granite-js/blur-view@1.0.33
9
+ - @granite-js/brownfield-module@1.0.33
10
+ - @granite-js/cli@1.0.33
11
+ - @granite-js/jest@1.0.33
12
+ - @granite-js/mpack@1.0.33
13
+ - @granite-js/native@1.0.33
14
+ - @granite-js/plugin-core@1.0.33
15
+ - @granite-js/style-utils@1.0.33
16
+ - @granite-js/video@1.0.33
17
+
18
+ ## 1.0.32
19
+
20
+ ### Patch Changes
21
+
22
+ - 20f2781: add opt-in back handler API
23
+ - @granite-js/blur-view@1.0.32
24
+ - @granite-js/brownfield-module@1.0.32
25
+ - @granite-js/cli@1.0.32
26
+ - @granite-js/jest@1.0.32
27
+ - @granite-js/mpack@1.0.32
28
+ - @granite-js/native@1.0.32
29
+ - @granite-js/plugin-core@1.0.32
30
+ - @granite-js/style-utils@1.0.32
31
+ - @granite-js/video@1.0.32
32
+
3
33
  ## 1.0.31
4
34
 
5
35
  ### Patch Changes
@@ -1 +1 @@
1
- export declare function AppRoot({ appName, context, container: Container, initialProps, initialScheme, router, setIosSwipeGestureEnabled, getInitialUrl, }: AppRootProps): import("react/jsx-runtime").JSX.Element;
1
+ export declare function AppRoot({ appName, context, container: Container, initialProps, initialScheme, router, setIosSwipeGestureEnabled, setiOSBackPressHandler, getInitialUrl, }: AppRootProps): import("react/jsx-runtime").JSX.Element;
@@ -31,6 +31,13 @@ export interface GraniteProps {
31
31
  setIosSwipeGestureEnabled?: ({ isEnabled }: {
32
32
  isEnabled: boolean;
33
33
  }) => void;
34
+ /**
35
+ * @description
36
+ * The function to register a handler that runs when the iOS swipe back gesture is detected.
37
+ */
38
+ setiOSBackPressHandler?: ({ handler }: {
39
+ handler: () => void;
40
+ }) => Promise<void> | void;
34
41
  /**
35
42
  * @description
36
43
  * The function to provide the initial URL to router.
@@ -74,7 +81,7 @@ export interface GraniteProps {
74
81
  * ```
75
82
  */
76
83
  export declare const Granite: {
77
- registerApp(AppContainer: ComponentType<PropsWithChildren<InitialProps>>, { appName, context, router, initialScheme, setIosSwipeGestureEnabled, getInitialUrl }: GraniteProps): (initialProps: InitialProps) => JSX.Element;
84
+ registerApp(AppContainer: ComponentType<PropsWithChildren<InitialProps>>, { appName, context, router, initialScheme, setIosSwipeGestureEnabled, setiOSBackPressHandler, getInitialUrl }: GraniteProps): (initialProps: InitialProps) => JSX.Element;
78
85
  registerHostApp(AppContainer: ComponentType<PropsWithChildren<InitialProps>>, { appName }: Pick<GraniteProps, "appName">): (initialProps: InitialProps) => React.JSX.Element;
79
86
  readonly appName: string;
80
87
  };
@@ -63,5 +63,5 @@ type NavigationContainerProps = Pick<ComponentProps<typeof NavigationContainer>,
63
63
  * }
64
64
  * ```
65
65
  */
66
- export declare function Router({ prefix, context, container: Container, initialProps, initialScheme, navigationContainerRef, defaultScreenOption, screenContainer, defaultErrorComponent, setIosSwipeGestureEnabled, getInitialUrl, ...navigationContainerProps }: InternalRouterProps & RouterProps): ReactElement;
66
+ export declare function Router({ prefix, context, container: Container, initialProps, initialScheme, navigationContainerRef, defaultScreenOption, screenContainer, defaultErrorComponent, setIosSwipeGestureEnabled, setiOSBackPressHandler, getInitialUrl, ...navigationContainerProps }: InternalRouterProps & RouterProps): ReactElement;
67
67
  export {};
@@ -1,10 +1,18 @@
1
1
  import { ReactNode } from 'react';
2
- export declare function CanGoBackGuard({ children, canGoBack, onBack, isInitialScreen, setIosSwipeGestureEnabled, }: {
2
+ import type { BackEvent } from '../../use-back-event';
3
+ type SetIosSwipeGestureEnabled = ({ isEnabled }: {
4
+ isEnabled: boolean;
5
+ }) => Promise<void> | void;
6
+ type SetIOSBackPressHandler = ({ handler }: {
7
+ handler: () => void;
8
+ }) => Promise<void> | void;
9
+ export declare function CanGoBackGuard({ children, canGoBack, hasBackEvent, onBack, isInitialScreen, setIosSwipeGestureEnabled, setiOSBackPressHandler, }: {
3
10
  canGoBack: boolean;
11
+ hasBackEvent: boolean;
4
12
  isInitialScreen: boolean;
5
13
  children: ReactNode;
6
- onBack?: () => void;
7
- setIosSwipeGestureEnabled?: ({ isEnabled }: {
8
- isEnabled: boolean;
9
- }) => void;
14
+ onBack?: (event: BackEvent) => void;
15
+ setIosSwipeGestureEnabled?: SetIosSwipeGestureEnabled;
16
+ setiOSBackPressHandler?: SetIOSBackPressHandler;
10
17
  }): import("react/jsx-runtime").JSX.Element;
18
+ export {};
@@ -1,4 +1,5 @@
1
- import { NavigationContainerRefWithCurrent } from '@granite-js/native/@react-navigation/native';
1
+ import type { NavigationContainerRefWithCurrent } from '@granite-js/native/@react-navigation/native';
2
+ import type { BackEvent } from '../../use-back-event';
2
3
  /**
3
4
  * @public
4
5
  * @name useRouterBackHandler
@@ -52,6 +53,8 @@ export declare function useInternalRouterBackHandler({ navigationContainerRef, o
52
53
  onClose?: () => void;
53
54
  }): {
54
55
  handler: () => void;
56
+ handleBackEvent: (event: BackEvent) => void;
55
57
  canGoBack: boolean;
58
+ hasBackEvent: boolean;
56
59
  onBack: () => void;
57
60
  };
@@ -0,0 +1,15 @@
1
+ import type { BackEvent, BackEventControls, BackEventHandler } from './useBackEvent';
2
+ import type { BackHandlerCallback, BackHandlerControls } from './useBackHandler';
3
+ export interface PrivateBackEventControls extends BackEventControls {
4
+ handlersRef: Set<BackEventHandler>;
5
+ hasBackEvent: boolean;
6
+ hasBackHandler: boolean;
7
+ addBackHandler: BackHandlerControls['addEventListener'];
8
+ removeBackHandler: (...handlers: Array<BackHandlerCallback>) => void;
9
+ onBack: () => void;
10
+ onBackHandler: (event: BackEvent) => boolean;
11
+ }
12
+ export type BackEventActionsContextValue = Omit<PrivateBackEventControls, 'handlersRef' | 'hasBackEvent' | 'hasBackHandler'>;
13
+ export type BackEventStateContextValue = Pick<PrivateBackEventControls, 'handlersRef' | 'hasBackEvent' | 'hasBackHandler'>;
14
+ export declare const BackEventActionsContext: import("react").Context<BackEventActionsContextValue | null>;
15
+ export declare const BackEventStateContext: import("react").Context<BackEventStateContextValue | null>;
@@ -1 +1,2 @@
1
1
  export * from './useBackEvent';
2
+ export * from './useBackHandler';
@@ -1,12 +1,13 @@
1
1
  import { ReactNode } from 'react';
2
- export interface BackEventControls {
3
- addEventListener: (...handlers: Array<() => void>) => void;
4
- removeEventListener: (...handlers: Array<() => void>) => void;
2
+ import { type PrivateBackEventControls } from './BackEventContext';
3
+ export type BackEventSource = 'backButton' | 'iosSwipeGesture' | 'androidHardwareBackPress';
4
+ export interface BackEvent {
5
+ source: BackEventSource;
5
6
  }
6
- interface PrivateBackEventControls extends BackEventControls {
7
- handlersRef: Set<() => void>;
8
- hasBackEvent: boolean;
9
- onBack: () => void;
7
+ export type BackEventHandler = () => void;
8
+ export interface BackEventControls {
9
+ addEventListener: (...handlers: Array<BackEventHandler>) => void;
10
+ removeEventListener: (...handlers: Array<BackEventHandler>) => void;
10
11
  }
11
12
  /**
12
13
  * @component
@@ -127,5 +128,3 @@ export declare function useBackEventState(): PrivateBackEventControls;
127
128
  * ```
128
129
  */
129
130
  export declare function useBackEvent(): BackEventControls;
130
- export declare function useBackEventContext(): PrivateBackEventControls;
131
- export {};
@@ -0,0 +1 @@
1
+ export declare function useBackEventActionsContext(): import("./BackEventContext").BackEventActionsContextValue;
@@ -0,0 +1,2 @@
1
+ import { type PrivateBackEventControls } from './BackEventContext';
2
+ export declare function useBackEventContext(): PrivateBackEventControls;
@@ -0,0 +1,53 @@
1
+ import type { BackEvent } from './useBackEvent';
2
+ export type BackHandlerCallback = (event: BackEvent) => boolean | void;
3
+ export interface BackHandlerSubscription {
4
+ remove: () => void;
5
+ }
6
+ export interface BackHandlerControls {
7
+ addEventListener: (handler: BackHandlerCallback) => BackHandlerSubscription;
8
+ }
9
+ /**
10
+ * @public
11
+ * @category Screen Control
12
+ * @name useBackHandler
13
+ * @description
14
+ * A Hook that registers back handlers. Back handlers run before handlers registered through `useBackEvent`.
15
+ * Return `true` to stop bubbling and prevent default back navigation. Return `false` or return nothing to continue bubbling to the next handler.
16
+ *
17
+ * @example
18
+ *
19
+ * ### Example of Closing an Overlay First
20
+ *
21
+ * ```tsx
22
+ * import { useEffect, useState } from 'react';
23
+ * import { Button, View } from 'react-native';
24
+ * import { useBackHandler } from '@granite-js/react-native';
25
+ *
26
+ * export function OverlayExample() {
27
+ * const backHandler = useBackHandler();
28
+ * const [isOverlayOpen, setIsOverlayOpen] = useState(false);
29
+ *
30
+ * useEffect(() => {
31
+ * const subscription = backHandler.addEventListener(() => {
32
+ * if (isOverlayOpen) {
33
+ * setIsOverlayOpen(false);
34
+ * return true;
35
+ * }
36
+ *
37
+ * return undefined;
38
+ * });
39
+ *
40
+ * return () => {
41
+ * subscription.remove();
42
+ * };
43
+ * }, [backHandler, isOverlayOpen]);
44
+ *
45
+ * return (
46
+ * <View>
47
+ * <Button title="Open Overlay" onPress={() => setIsOverlayOpen(true)} />
48
+ * </View>
49
+ * );
50
+ * }
51
+ * ```
52
+ */
53
+ export declare function useBackHandler(): BackHandlerControls;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granite-js/react-native",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "The Granite Framework",
5
5
  "bin": {
6
6
  "granite": "./bin/cli.js"
@@ -91,7 +91,8 @@
91
91
  "@babel/core": "7.28.5",
92
92
  "@babel/preset-env": "7.28.5",
93
93
  "@babel/preset-typescript": "7.28.5",
94
- "@granite-js/native": "1.0.31",
94
+ "@granite-js/native": "1.0.33",
95
+ "@granite-js/vitest": "1.0.33",
95
96
  "@testing-library/dom": "^10.4.0",
96
97
  "@testing-library/react": "^16.1.0",
97
98
  "@types/babel__core": "^7",
@@ -113,21 +114,21 @@
113
114
  "zod": "^4.1.12"
114
115
  },
115
116
  "peerDependencies": {
116
- "@granite-js/native": "1.0.31",
117
+ "@granite-js/native": "1.0.33",
117
118
  "@types/react": "*",
118
119
  "brick-module": "*",
119
120
  "react": "*",
120
121
  "react-native": "*"
121
122
  },
122
123
  "dependencies": {
123
- "@granite-js/blur-view": "1.0.31",
124
- "@granite-js/brownfield-module": "1.0.31",
125
- "@granite-js/cli": "1.0.31",
126
- "@granite-js/jest": "1.0.31",
127
- "@granite-js/mpack": "1.0.31",
128
- "@granite-js/plugin-core": "1.0.31",
129
- "@granite-js/style-utils": "1.0.31",
130
- "@granite-js/video": "1.0.31",
124
+ "@granite-js/blur-view": "1.0.33",
125
+ "@granite-js/brownfield-module": "1.0.33",
126
+ "@granite-js/cli": "1.0.33",
127
+ "@granite-js/jest": "1.0.33",
128
+ "@granite-js/mpack": "1.0.33",
129
+ "@granite-js/plugin-core": "1.0.33",
130
+ "@granite-js/style-utils": "1.0.33",
131
+ "@granite-js/video": "1.0.33",
131
132
  "@standard-schema/spec": "^1.0.0",
132
133
  "es-toolkit": "^1.39.8",
133
134
  "react-native-url-polyfill": "3.0.0"
@@ -16,6 +16,7 @@ interface AppRootProps extends GraniteProps {
16
16
  initialProps: InitialProps;
17
17
  initialScheme: string;
18
18
  setIosSwipeGestureEnabled?: ({ isEnabled }: { isEnabled: boolean }) => void;
19
+ setiOSBackPressHandler?: ({ handler }: { handler: () => void }) => Promise<void> | void;
19
20
  getInitialUrl: InternalRouterProps['getInitialUrl'];
20
21
  }
21
22
 
@@ -27,6 +28,7 @@ export function AppRoot({
27
28
  initialScheme,
28
29
  router,
29
30
  setIosSwipeGestureEnabled,
31
+ setiOSBackPressHandler,
30
32
  getInitialUrl,
31
33
  }: AppRootProps) {
32
34
  const prefix = getSchemePrefix({
@@ -47,6 +49,7 @@ export function AppRoot({
47
49
  container={Container}
48
50
  prefix={prefix}
49
51
  setIosSwipeGestureEnabled={setIosSwipeGestureEnabled}
52
+ setiOSBackPressHandler={setiOSBackPressHandler}
50
53
  getInitialUrl={getInitialUrl}
51
54
  {...router}
52
55
  />
@@ -39,6 +39,12 @@ export interface GraniteProps {
39
39
  */
40
40
  setIosSwipeGestureEnabled?: ({ isEnabled }: { isEnabled: boolean }) => void;
41
41
 
42
+ /**
43
+ * @description
44
+ * The function to register a handler that runs when the iOS swipe back gesture is detected.
45
+ */
46
+ setiOSBackPressHandler?: ({ handler }: { handler: () => void }) => Promise<void> | void;
47
+
42
48
  /**
43
49
  * @description
44
50
  * The function to provide the initial URL to router.
@@ -65,7 +71,7 @@ const createApp = () => {
65
71
  return {
66
72
  registerApp(
67
73
  AppContainer: ComponentType<PropsWithChildren<InitialProps>>,
68
- { appName, context, router, initialScheme, setIosSwipeGestureEnabled, getInitialUrl }: GraniteProps
74
+ { appName, context, router, initialScheme, setIosSwipeGestureEnabled, setiOSBackPressHandler, getInitialUrl }: GraniteProps
69
75
  ): (initialProps: InitialProps) => JSX.Element {
70
76
  if (appName === ENTRY_BUNDLE_NAME) {
71
77
  throw new Error(`Reserved app name 'shared' cannot be used`);
@@ -80,6 +86,7 @@ const createApp = () => {
80
86
  initialProps={initialProps}
81
87
  initialScheme={initialSchemeValue}
82
88
  setIosSwipeGestureEnabled={setIosSwipeGestureEnabled}
89
+ setiOSBackPressHandler={setiOSBackPressHandler}
83
90
  getInitialUrl={getInitialUrl}
84
91
  appName={appName}
85
92
  context={context}
@@ -159,12 +159,12 @@ class IntersectionObserver {
159
159
  if (horizontal) {
160
160
  const visibleTargetMinX = Math.max(contentOffset.x - (rootMargin.left || 0), targetLayout.x);
161
161
  const visibleTargetMaxX = Math.min(
162
- contentOffsetWithLayout + (rootMargin.left || 0),
162
+ contentOffsetWithLayout + (rootMargin.right || 0),
163
163
  targetLayout.x + targetLayout.width
164
164
  );
165
- const visibleHeight = Math.max(visibleTargetMaxX - visibleTargetMinX, 0);
165
+ const visibleWidth = Math.max(visibleTargetMaxX - visibleTargetMinX, 0);
166
166
 
167
- intersectionRatio = visibleHeight / targetLayout.height;
167
+ intersectionRatio = visibleWidth / targetLayout.width;
168
168
  isIntersecting =
169
169
  contentOffsetWithLayout + (rootMargin.right || 0) >= targetLayout.x &&
170
170
  contentOffset.x - (rootMargin.left || 0) <= targetLayout.x + targetLayout.width;
@@ -65,6 +65,7 @@ export interface InternalRouterProps {
65
65
  initialProps: InitialProps;
66
66
  initialScheme: string;
67
67
  setIosSwipeGestureEnabled?: ({ isEnabled }: { isEnabled: boolean }) => Promise<void> | void;
68
+ setiOSBackPressHandler?: ({ handler }: { handler: () => void }) => Promise<void> | void;
68
69
  getInitialUrl?: RouterControlsConfig['getInitialUrl'];
69
70
  }
70
71
 
@@ -148,6 +149,7 @@ export function Router({
148
149
  defaultErrorComponent,
149
150
  // Public props (StackNavigator)
150
151
  setIosSwipeGestureEnabled,
152
+ setiOSBackPressHandler,
151
153
  getInitialUrl,
152
154
  ...navigationContainerProps
153
155
  }: InternalRouterProps & RouterProps): ReactElement {
@@ -162,7 +164,7 @@ export function Router({
162
164
 
163
165
  const ref = useMemo(() => navigationContainerRef ?? createNavigationContainerRef<never>(), [navigationContainerRef]);
164
166
 
165
- const { handler, canGoBack, onBack } = useInternalRouterBackHandler({
167
+ const { handler, handleBackEvent, canGoBack, hasBackEvent } = useInternalRouterBackHandler({
166
168
  navigationContainerRef: ref,
167
169
  onClose: closeView,
168
170
  });
@@ -195,9 +197,11 @@ export function Router({
195
197
  >
196
198
  <CanGoBackGuard
197
199
  canGoBack={canGoBack}
200
+ hasBackEvent={hasBackEvent}
198
201
  isInitialScreen={isInitialScreen}
199
- onBack={onBack}
202
+ onBack={handleBackEvent}
200
203
  setIosSwipeGestureEnabled={setIosSwipeGestureEnabled}
204
+ setiOSBackPressHandler={setiOSBackPressHandler}
201
205
  >
202
206
  <Container {...initialProps}>
203
207
  <StackNavigator.Navigator screenOptions={screenOptions}>{Screens}</StackNavigator.Navigator>
@@ -0,0 +1,85 @@
1
+ import { render } from '@testing-library/react';
2
+ import { BackHandler } from 'react-native';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { CanGoBackGuard } from './CanGoBackGuard';
5
+
6
+ describe('CanGoBackGuard', () => {
7
+ beforeEach(() => {
8
+ vi.mocked(BackHandler.addEventListener).mockClear();
9
+ });
10
+
11
+ it('passes Android hardware back events to onBack and consumes the native event', () => {
12
+ const onBack = vi.fn();
13
+
14
+ render(
15
+ <CanGoBackGuard canGoBack={false} hasBackEvent={true} isInitialScreen={true} onBack={onBack}>
16
+ <div />
17
+ </CanGoBackGuard>
18
+ );
19
+
20
+ const handler = vi.mocked(BackHandler.addEventListener).mock.calls[0]?.[1];
21
+
22
+ expect(BackHandler.addEventListener).toHaveBeenCalledWith('hardwareBackPress', expect.any(Function));
23
+ expect(handler?.()).toBe(true);
24
+ expect(onBack).toHaveBeenCalledWith({ source: 'androidHardwareBackPress' });
25
+ });
26
+
27
+ it('does not register Android hardware back when no back event exists', () => {
28
+ const onBack = vi.fn();
29
+
30
+ render(
31
+ <CanGoBackGuard canGoBack={true} hasBackEvent={false} isInitialScreen={true} onBack={onBack}>
32
+ <div />
33
+ </CanGoBackGuard>
34
+ );
35
+
36
+ expect(BackHandler.addEventListener).not.toHaveBeenCalled();
37
+ });
38
+
39
+ it('registers the iOS swipe back handler while back events exist', () => {
40
+ const onBack = vi.fn();
41
+ const setiOSBackPressHandler = vi.fn();
42
+
43
+ const { unmount } = render(
44
+ <CanGoBackGuard
45
+ canGoBack={true}
46
+ hasBackEvent={true}
47
+ isInitialScreen={true}
48
+ onBack={onBack}
49
+ setiOSBackPressHandler={setiOSBackPressHandler}
50
+ >
51
+ <div />
52
+ </CanGoBackGuard>
53
+ );
54
+
55
+ setiOSBackPressHandler.mock.calls[0]?.[0].handler();
56
+
57
+ expect(setiOSBackPressHandler).toHaveBeenCalledWith({ handler: expect.any(Function) });
58
+ expect(onBack).toHaveBeenCalledWith({ source: 'iosSwipeGesture' });
59
+
60
+ unmount();
61
+
62
+ expect(setiOSBackPressHandler).toHaveBeenLastCalledWith({ handler: expect.any(Function) });
63
+ });
64
+
65
+ it('disables iOS swipe when the current state should block default back navigation', () => {
66
+ const setIosSwipeGestureEnabled = vi.fn();
67
+
68
+ const { unmount } = render(
69
+ <CanGoBackGuard
70
+ canGoBack={false}
71
+ hasBackEvent={true}
72
+ isInitialScreen={true}
73
+ setIosSwipeGestureEnabled={setIosSwipeGestureEnabled}
74
+ >
75
+ <div />
76
+ </CanGoBackGuard>
77
+ );
78
+
79
+ expect(setIosSwipeGestureEnabled).toHaveBeenCalledWith({ isEnabled: false });
80
+
81
+ unmount();
82
+
83
+ expect(setIosSwipeGestureEnabled).toHaveBeenLastCalledWith({ isEnabled: true });
84
+ });
85
+ });
@@ -1,21 +1,27 @@
1
1
  import { ReactNode, useEffect } from 'react';
2
2
  import { BackHandler } from 'react-native';
3
+ import type { BackEvent } from '../../use-back-event';
4
+
5
+ type SetIosSwipeGestureEnabled = ({ isEnabled }: { isEnabled: boolean }) => Promise<void> | void;
6
+ type SetIOSBackPressHandler = ({ handler }: { handler: () => void }) => Promise<void> | void;
3
7
 
4
8
  export function CanGoBackGuard({
5
9
  children,
6
10
  canGoBack,
11
+ hasBackEvent,
7
12
  onBack,
8
13
  isInitialScreen,
9
14
  setIosSwipeGestureEnabled,
15
+ setiOSBackPressHandler,
10
16
  }: {
11
17
  canGoBack: boolean;
18
+ hasBackEvent: boolean;
12
19
  isInitialScreen: boolean;
13
20
  children: ReactNode;
14
- onBack?: () => void;
15
- setIosSwipeGestureEnabled?: ({ isEnabled }: { isEnabled: boolean }) => void;
21
+ onBack?: (event: BackEvent) => void;
22
+ setIosSwipeGestureEnabled?: SetIosSwipeGestureEnabled;
23
+ setiOSBackPressHandler?: SetIOSBackPressHandler;
16
24
  }) {
17
- const shouldBlockGoingBack = !canGoBack;
18
-
19
25
  useEffect(() => {
20
26
  if (!isInitialScreen || !canGoBack) {
21
27
  setIosSwipeGestureEnabled?.({ isEnabled: false });
@@ -29,19 +35,36 @@ export function CanGoBackGuard({
29
35
  }, [canGoBack, isInitialScreen, setIosSwipeGestureEnabled]);
30
36
 
31
37
  useEffect(() => {
32
- if (shouldBlockGoingBack) {
33
- const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
34
- onBack?.();
35
- return true;
36
- });
38
+ if (!hasBackEvent) {
39
+ return;
40
+ }
37
41
 
38
- return () => {
39
- subscription.remove();
40
- };
42
+ const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
43
+ onBack?.({ source: 'androidHardwareBackPress' });
44
+
45
+ return true;
46
+ });
47
+
48
+ return () => {
49
+ subscription.remove();
50
+ };
51
+ }, [hasBackEvent, onBack]);
52
+
53
+ useEffect(() => {
54
+ if (!hasBackEvent || setiOSBackPressHandler == null) {
55
+ return;
41
56
  }
42
57
 
43
- return;
44
- }, [shouldBlockGoingBack, onBack]);
58
+ setiOSBackPressHandler({
59
+ handler: () => {
60
+ onBack?.({ source: 'iosSwipeGesture' });
61
+ },
62
+ });
63
+
64
+ return () => {
65
+ setiOSBackPressHandler({ handler: () => {} });
66
+ };
67
+ }, [hasBackEvent, onBack, setiOSBackPressHandler]);
45
68
 
46
69
  return <>{children}</>;
47
70
  }