chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,16 @@
import React from 'react';
import { GestureProviderProps } from '../types';
import { GHContext } from '../contexts';
import ScreenGestureDetector from './ScreenGestureDetector';
function GHWrapper(props: GestureProviderProps) {
return <ScreenGestureDetector {...props} />;
}
export default function GestureDetectorProvider(props: {
children: React.ReactNode;
}) {
return (
<GHContext.Provider value={GHWrapper}>{props.children}</GHContext.Provider>
);
}
@@ -0,0 +1,13 @@
type RNScreensTurboModuleType = {
startTransition: (stackTag: number) => {
topScreenTag: number;
belowTopScreenTag: number;
canStartTransition: boolean;
};
updateTransition: (stackTag: number, progress: number) => void;
finishTransition: (stackTag: number, isCanceled: boolean) => void;
disableSwipeBackForTopScreen: (stackTag: number) => void;
};
export const RNScreensTurboModule: RNScreensTurboModuleType = (global as any)
.RNScreensTurboModule;
@@ -0,0 +1,255 @@
import React, { useEffect } from 'react';
import { Dimensions, Platform, findNodeHandle } from 'react-native';
import {
GestureDetector,
Gesture,
PanGestureHandlerEventPayload,
GestureUpdateEvent,
} from 'react-native-gesture-handler';
import {
useSharedValue,
measure,
startScreenTransition,
finishScreenTransition,
makeMutable,
runOnUI,
} from 'react-native-reanimated';
import { getShadowNodeWrapperAndTagFromRef, isFabric } from './fabricUtils';
import { RNScreensTurboModule } from './RNScreensTurboModule';
import { DefaultEvent, DefaultScreenDimensions } from './defaults';
import {
checkBoundaries,
checkIfTransitionCancelled,
getAnimationForTransition,
} from './constraints';
import { GestureProviderProps } from '../types';
// The detector is disabled to work around issue with pressables
// losing focus. See https://github.com/software-mansion/react-native-screens/pull/2819
const EmptyGestureHandler = Gesture.Fling().enabled(false);
const ScreenGestureDetector = ({
children,
gestureDetectorBridge,
goBackGesture,
screenEdgeGesture,
transitionAnimation: customTransitionAnimation,
screensRefs,
currentScreenId,
}: GestureProviderProps) => {
const sharedEvent = useSharedValue(DefaultEvent);
const startingGesturePosition = useSharedValue(DefaultEvent);
const canPerformUpdates = makeMutable(false);
const transitionAnimation = getAnimationForTransition(
goBackGesture,
customTransitionAnimation,
);
const screenTransitionConfig = makeMutable({
stackTag: -1,
belowTopScreenId: -1,
topScreenId: -1,
sharedEvent,
startingGesturePosition,
screenTransition: transitionAnimation,
isTransitionCanceled: false,
goBackGesture: goBackGesture ?? 'swipeRight',
screenDimensions: DefaultScreenDimensions,
onFinishAnimation: () => {
'worklet';
},
});
const stackTag = makeMutable(-1);
const screenTagToNodeWrapperUI = makeMutable<Record<string, any>>({});
const IS_FABRIC = isFabric();
gestureDetectorBridge.current.stackUseEffectCallback = stackRef => {
if (!goBackGesture) {
return;
}
stackTag.value = findNodeHandle(stackRef.current as any) as number;
if (Platform.OS === 'ios') {
runOnUI(() => {
RNScreensTurboModule.disableSwipeBackForTopScreen(stackTag.value);
})();
}
};
useEffect(() => {
if (!IS_FABRIC || !goBackGesture || screensRefs === undefined) {
return;
}
const screenTagToNodeWrapper: Record<string, Record<string, unknown>> = {};
for (const key in screensRefs.current) {
const screenRef = screensRefs.current[key];
const screenData = getShadowNodeWrapperAndTagFromRef(screenRef.current);
if (screenData.tag && screenData.shadowNodeWrapper) {
screenTagToNodeWrapper[screenData.tag] = screenData.shadowNodeWrapper;
} else {
console.warn('[RNScreens] Failed to find tag for screen.');
}
}
screenTagToNodeWrapperUI.value = screenTagToNodeWrapper;
}, [currentScreenId, goBackGesture]);
function computeProgress(
event: GestureUpdateEvent<PanGestureHandlerEventPayload>,
) {
'worklet';
let progress = 0;
const screenDimensions = screenTransitionConfig.value.screenDimensions;
const startingPosition = startingGesturePosition.value;
if (goBackGesture === 'swipeRight') {
progress =
event.translationX /
(screenDimensions.width - startingPosition.absoluteX);
} else if (goBackGesture === 'swipeLeft') {
progress = (-1 * event.translationX) / startingPosition.absoluteX;
} else if (goBackGesture === 'swipeDown') {
progress =
(-1 * event.translationY) /
(screenDimensions.height - startingPosition.absoluteY);
} else if (goBackGesture === 'swipeUp') {
progress = event.translationY / startingPosition.absoluteY;
} else if (goBackGesture === 'horizontalSwipe') {
progress = Math.abs(event.translationX / screenDimensions.width / 2);
} else if (goBackGesture === 'verticalSwipe') {
progress = Math.abs(event.translationY / screenDimensions.height / 2);
} else if (goBackGesture === 'twoDimensionalSwipe') {
const progressX = Math.abs(
event.translationX / screenDimensions.width / 2,
);
const progressY = Math.abs(
event.translationY / screenDimensions.height / 2,
);
progress = Math.max(progressX, progressY);
}
return progress;
}
function onStart(event: GestureUpdateEvent<PanGestureHandlerEventPayload>) {
'worklet';
sharedEvent.value = event;
const transitionConfig = screenTransitionConfig.value;
const transitionData = RNScreensTurboModule.startTransition(stackTag.value);
if (transitionData.canStartTransition === false) {
canPerformUpdates.value = false;
return;
}
if (IS_FABRIC) {
transitionConfig.topScreenId =
screenTagToNodeWrapperUI.value[transitionData.topScreenTag];
transitionConfig.belowTopScreenId =
screenTagToNodeWrapperUI.value[transitionData.belowTopScreenTag];
} else {
transitionConfig.topScreenId = transitionData.topScreenTag;
transitionConfig.belowTopScreenId = transitionData.belowTopScreenTag;
}
transitionConfig.stackTag = stackTag.value;
startingGesturePosition.value = event;
const animatedRefMock = () => {
return screenTransitionConfig.value.topScreenId;
};
const screenSize = measure(animatedRefMock as any);
if (screenSize == null) {
throw new Error('[RNScreens] Failed to measure screen.');
}
if (screenSize == null) {
canPerformUpdates.value = false;
RNScreensTurboModule.finishTransition(stackTag.value, true);
return;
}
transitionConfig.screenDimensions = screenSize;
// Gesture Handler added `pointerType` to event payload back in 2.16.0,
// see: https://github.com/software-mansion/react-native-gesture-handler/pull/2760
// and this causes type errors here. Proper solution would be to patch parameter types
// of this function in reanimated. This should not cause runtime errors as the payload
// has correct shape, only the types are incorrect.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
startScreenTransition(transitionConfig as any);
canPerformUpdates.value = true;
}
function onUpdate(event: GestureUpdateEvent<PanGestureHandlerEventPayload>) {
'worklet';
if (!canPerformUpdates.value) {
return;
}
checkBoundaries(goBackGesture, event);
const progress = computeProgress(event);
sharedEvent.value = event;
const stackTag = screenTransitionConfig.value.stackTag;
RNScreensTurboModule.updateTransition(stackTag, progress);
}
function onEnd(event: GestureUpdateEvent<PanGestureHandlerEventPayload>) {
'worklet';
if (!canPerformUpdates.value) {
return;
}
const velocityFactor = 0.3;
const screenSize = screenTransitionConfig.value.screenDimensions;
const distanceX =
event.translationX + Math.min(event.velocityX * velocityFactor, 100);
const distanceY =
event.translationY + Math.min(event.velocityY * velocityFactor, 100);
const requiredXDistance = screenSize.width / 2;
const requiredYDistance = screenSize.height / 2;
const isTransitionCanceled = checkIfTransitionCancelled(
goBackGesture,
distanceX,
requiredXDistance,
distanceY,
requiredYDistance,
);
const stackTag = screenTransitionConfig.value.stackTag;
screenTransitionConfig.value.onFinishAnimation = () => {
RNScreensTurboModule.finishTransition(stackTag, isTransitionCanceled);
};
screenTransitionConfig.value.isTransitionCanceled = isTransitionCanceled;
// Gesture Handler added `pointerType` to event payload back in 2.16.0,
// see: https://github.com/software-mansion/react-native-gesture-handler/pull/2760
// and this causes type errors here. Proper solution would be to patch parameter types
// of this function in reanimated. This should not cause runtime errors as the payload
// has correct shape, only the types are incorrect.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
finishScreenTransition(screenTransitionConfig.value as any);
}
let panGesture = Gesture.Pan()
.onStart(onStart)
.onUpdate(onUpdate)
.onEnd(onEnd);
if (screenEdgeGesture) {
const HIT_SLOP_SIZE = 50;
const ACTIVATION_DISTANCE = 30;
if (goBackGesture === 'swipeRight') {
panGesture = panGesture
.activeOffsetX(ACTIVATION_DISTANCE)
.hitSlop({ left: 0, top: 0, width: HIT_SLOP_SIZE });
} else if (goBackGesture === 'swipeLeft') {
panGesture = panGesture
.activeOffsetX(-ACTIVATION_DISTANCE)
.hitSlop({ right: 0, top: 0, width: HIT_SLOP_SIZE });
} else if (goBackGesture === 'swipeDown') {
panGesture = panGesture
.activeOffsetY(ACTIVATION_DISTANCE)
.hitSlop({ top: 0, height: Dimensions.get('window').height * 0.2 });
// workaround, because we don't have access to header height
} else if (goBackGesture === 'swipeUp') {
panGesture = panGesture
.activeOffsetY(-ACTIVATION_DISTANCE)
.hitSlop({ bottom: 0, height: HIT_SLOP_SIZE });
}
}
return (
<GestureDetector gesture={goBackGesture ? panGesture : EmptyGestureHandler}>
{children}
</GestureDetector>
);
};
export default ScreenGestureDetector;
@@ -0,0 +1,87 @@
import { ScreenTransition } from 'react-native-reanimated';
import {
AnimatedScreenTransition,
GoBackGesture,
PanGestureHandlerEventPayload,
} from '../native-stack/types';
import { AnimationForGesture } from './defaults';
import { GestureUpdateEvent } from 'react-native-gesture-handler';
const SupportedGestures = [
'swipeRight',
'swipeLeft',
'swipeDown',
'swipeUp',
'horizontalSwipe',
'verticalSwipe',
'twoDimensionalSwipe',
];
export function getAnimationForTransition(
goBackGesture: GoBackGesture | undefined,
customTransitionAnimation: AnimatedScreenTransition | undefined,
) {
let transitionAnimation = ScreenTransition.SwipeRight;
if (customTransitionAnimation) {
transitionAnimation = customTransitionAnimation;
if (!goBackGesture) {
throw new Error(
'[RNScreens] You have to specify `goBackGesture` when using `transitionAnimation`.',
);
}
} else {
if (!!goBackGesture && SupportedGestures.includes(goBackGesture)) {
transitionAnimation = AnimationForGesture[goBackGesture];
} else if (goBackGesture !== undefined) {
throw new Error(
`[RNScreens] Unknown goBackGesture parameter has been specified: ${goBackGesture}.`,
);
}
}
return transitionAnimation;
}
export function checkBoundaries(
goBackGesture: string | undefined,
event: GestureUpdateEvent<PanGestureHandlerEventPayload>,
) {
'worklet';
if (goBackGesture === 'swipeRight' && event.translationX < 0) {
event.translationX = 0;
} else if (goBackGesture === 'swipeLeft' && event.translationX > 0) {
event.translationX = 0;
} else if (goBackGesture === 'swipeDown' && event.translationY < 0) {
event.translationY = 0;
} else if (goBackGesture === 'swipeUp' && event.translationY > 0) {
event.translationY = 0;
}
}
export function checkIfTransitionCancelled(
goBackGesture: string | undefined,
distanceX: number,
requiredXDistance: number,
distanceY: number,
requiredYDistance: number,
) {
'worklet';
let isTransitionCanceled = false;
if (goBackGesture === 'swipeRight') {
isTransitionCanceled = distanceX < requiredXDistance;
} else if (goBackGesture === 'swipeLeft') {
isTransitionCanceled = -distanceX < requiredXDistance;
} else if (goBackGesture === 'horizontalSwipe') {
isTransitionCanceled = Math.abs(distanceX) < requiredXDistance;
} else if (goBackGesture === 'swipeUp') {
isTransitionCanceled = -distanceY < requiredYDistance;
} else if (goBackGesture === 'swipeDown') {
isTransitionCanceled = distanceY < requiredYDistance;
} else if (goBackGesture === 'verticalSwipe') {
isTransitionCanceled = Math.abs(distanceY) < requiredYDistance;
} else if (goBackGesture === 'twoDimensionalSwipe') {
const isCanceledHorizontally = Math.abs(distanceX) < requiredXDistance;
const isCanceledVertically = Math.abs(distanceY) < requiredYDistance;
isTransitionCanceled = isCanceledHorizontally && isCanceledVertically;
}
return isTransitionCanceled;
}
@@ -0,0 +1,45 @@
import {
GestureUpdateEvent,
PanGestureHandlerEventPayload,
PointerType,
} from 'react-native-gesture-handler';
import { ScreenTransition } from 'react-native-reanimated';
export const DefaultEvent: GestureUpdateEvent<PanGestureHandlerEventPayload> = {
absoluteX: 0,
absoluteY: 0,
handlerTag: 0,
numberOfPointers: 0,
state: 0,
translationX: 0,
translationY: 0,
velocityX: 0,
velocityY: 0,
x: 0,
y: 0,
// These two were added in recent versions of gesture handler
// and they are required to specify. This should be backward
// compatible unless they strictly parse the objects, which seems
// not likely. PointerType is present since 2.16.0, StylusData since 2.20.0
pointerType: PointerType.TOUCH,
};
export const DefaultScreenDimensions = {
width: 0,
height: 0,
x: 0,
y: 0,
pageX: 0,
pageY: 0,
};
export const AnimationForGesture = {
swipeRight: ScreenTransition.SwipeRight,
swipeLeft: ScreenTransition.SwipeLeft,
swipeDown: ScreenTransition.SwipeDown,
swipeUp: ScreenTransition.SwipeUp,
horizontalSwipe: ScreenTransition.Horizontal,
verticalSwipe: ScreenTransition.Vertical,
twoDimensionalSwipe: ScreenTransition.TwoDimensional,
};
@@ -0,0 +1,86 @@
'use strict';
import { View } from 'react-native';
/* eslint-disable */
type LocalGlobal = typeof global & Record<string, unknown>;
export function isFabric() {
return !!(global as LocalGlobal).RN$Bridgeless;
}
export type ShadowNodeWrapper = {
__hostObjectShadowNodeWrapper: never;
};
let findHostInstance_DEPRECATED: (ref: unknown) => void;
let getInternalInstanceHandleFromPublicInstance: (ref: unknown) => {
stateNode: { node: unknown };
};
// Taken and modifies from reanimated
export function getShadowNodeWrapperAndTagFromRef(ref: View | null): {
shadowNodeWrapper: ShadowNodeWrapper;
tag: number;
} {
// load findHostInstance_DEPRECATED lazily because it may not be available before render
if (findHostInstance_DEPRECATED === undefined) {
try {
findHostInstance_DEPRECATED =
require('react-native/Libraries/Renderer/shims/ReactFabric').findHostInstance_DEPRECATED;
} catch (e) {
findHostInstance_DEPRECATED = (_ref: unknown) => null;
}
}
if (getInternalInstanceHandleFromPublicInstance === undefined) {
try {
getInternalInstanceHandleFromPublicInstance =
require('react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance')
.getInternalInstanceHandleFromPublicInstance ??
((_ref: any) => _ref._internalInstanceHandle);
} catch (e) {
getInternalInstanceHandleFromPublicInstance = (_ref: any) =>
_ref._internalInstanceHandle;
}
}
// taken from https://github.com/facebook/react-native/commit/803bb16531697233686efd475f004c1643e03617#diff-d8172256c6d63b5d32db10e54d7b10f37a26b337d5280d89f5bfd7bcea778292R196
// @ts-ignore some weird stuff on RN 0.74 - see examples with scrollView
const scrollViewRef = ref?.getScrollResponder?.()?.getNativeScrollRef?.();
// @ts-ignore some weird stuff on RN 0.74 - see examples with scrollView
const otherScrollViewRef = ref?.getNativeScrollRef?.();
// @ts-ignore some weird stuff on RN 0.74 - see setNativeProps example
const textInputRef = ref?.__internalInstanceHandle?.stateNode?.node;
let resolvedRef;
if (scrollViewRef) {
resolvedRef = {
shadowNodeWrapper: scrollViewRef.__internalInstanceHandle.stateNode.node,
tag: scrollViewRef._nativeTag,
};
} else if (otherScrollViewRef) {
resolvedRef = {
shadowNodeWrapper:
otherScrollViewRef.__internalInstanceHandle.stateNode.node,
tag: otherScrollViewRef.__nativeTag,
};
} else if (textInputRef) {
resolvedRef = {
shadowNodeWrapper: textInputRef,
tag: (ref as any)?.__nativeTag,
};
} else {
const hostInstance = findHostInstance_DEPRECATED(ref);
resolvedRef = {
shadowNodeWrapper:
getInternalInstanceHandleFromPublicInstance(hostInstance).stateNode
.node,
tag: (hostInstance as any)?._nativeTag,
};
}
return resolvedRef;
}
@@ -0,0 +1,10 @@
export function isFabric() {
return false;
}
export function getShadowNodeWrapperAndTagFromRef() {
return {
shadowNodeWrapper: undefined,
tag: undefined,
};
}
@@ -0,0 +1,4 @@
/*
* Providers
*/
export { default as GestureDetectorProvider } from './GestureDetectorProvider';