feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,191 @@
'use strict';
import type { StyleProps } from '../reanimated2';
import type {
IAnimatedComponentInternal,
AnimatedComponentProps,
IInlinePropManager,
ViewInfo,
} from './commonTypes';
import { flattenArray } from './utils';
import { makeViewDescriptorsSet } from '../reanimated2/ViewDescriptorsSet';
import type {
ViewDescriptorsSet,
ViewRefSet,
} from '../reanimated2/ViewDescriptorsSet';
import { adaptViewConfig } from '../ConfigHelper';
import updateProps from '../reanimated2/UpdateProps';
import { stopMapper, startMapper } from '../reanimated2/mappers';
import { isSharedValue } from '../reanimated2/isSharedValue';
import { shouldBeUseWeb } from '../reanimated2/PlatformChecker';
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
function isInlineStyleTransform(transform: unknown): boolean {
if (!Array.isArray(transform)) {
return false;
}
return transform.some((t: Record<string, unknown>) => hasInlineStyles(t));
}
function inlinePropsHasChanged(
styles1: StyleProps,
styles2: StyleProps
): boolean {
if (Object.keys(styles1).length !== Object.keys(styles2).length) {
return true;
}
for (const key of Object.keys(styles1)) {
if (styles1[key] !== styles2[key]) return true;
}
return false;
}
function getInlinePropsUpdate(inlineProps: Record<string, unknown>) {
'worklet';
const update: Record<string, unknown> = {};
for (const [key, styleValue] of Object.entries(inlineProps)) {
if (isSharedValue(styleValue)) {
update[key] = styleValue.value;
} else if (Array.isArray(styleValue)) {
update[key] = styleValue.map((item) => {
return getInlinePropsUpdate(item);
});
} else if (typeof styleValue === 'object') {
update[key] = getInlinePropsUpdate(styleValue as Record<string, unknown>);
} else {
update[key] = styleValue;
}
}
return update;
}
function extractSharedValuesMapFromProps(
props: AnimatedComponentProps<
Record<string, unknown> /* Initial component props */
>
): Record<string, unknown> {
const inlineProps: Record<string, unknown> = {};
for (const key in props) {
const value = props[key];
if (key === 'style') {
const styles = flattenArray<StyleProps>(props.style ?? []);
styles.forEach((style) => {
if (!style) {
return;
}
for (const [styleKey, styleValue] of Object.entries(style)) {
if (isSharedValue(styleValue)) {
inlineProps[styleKey] = styleValue;
} else if (
styleKey === 'transform' &&
isInlineStyleTransform(styleValue)
) {
inlineProps[styleKey] = styleValue;
}
}
});
} else if (isSharedValue(value)) {
inlineProps[key] = value;
}
}
return inlineProps;
}
export function hasInlineStyles(style: StyleProps): boolean {
if (!style) {
return false;
}
return Object.keys(style).some((key) => {
const styleValue = style[key];
return (
isSharedValue(styleValue) ||
(key === 'transform' && isInlineStyleTransform(styleValue))
);
});
}
export function getInlineStyle(
style: Record<string, unknown>,
shouldGetInitialStyle: boolean
) {
if (shouldGetInitialStyle) {
return getInlinePropsUpdate(style);
}
const newStyle: StyleProps = {};
for (const [key, styleValue] of Object.entries(style)) {
if (
!isSharedValue(styleValue) &&
!(key === 'transform' && isInlineStyleTransform(styleValue))
) {
newStyle[key] = styleValue;
}
}
return newStyle;
}
export class InlinePropManager implements IInlinePropManager {
_inlinePropsViewDescriptors: ViewDescriptorsSet | null = null;
_inlinePropsMapperId: number | null = null;
_inlineProps: StyleProps = {};
public attachInlineProps(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal,
viewInfo: ViewInfo
) {
const newInlineProps: Record<string, unknown> =
extractSharedValuesMapFromProps(animatedComponent.props);
const hasChanged = inlinePropsHasChanged(newInlineProps, this._inlineProps);
if (hasChanged) {
if (!this._inlinePropsViewDescriptors) {
this._inlinePropsViewDescriptors = makeViewDescriptorsSet();
const { viewTag, viewName, shadowNodeWrapper, viewConfig } = viewInfo;
if (Object.keys(newInlineProps).length && viewConfig) {
adaptViewConfig(viewConfig);
}
this._inlinePropsViewDescriptors.add({
tag: viewTag as number,
name: viewName!,
shadowNodeWrapper: shadowNodeWrapper!,
});
}
const shareableViewDescriptors =
this._inlinePropsViewDescriptors.shareableViewDescriptors;
const maybeViewRef = SHOULD_BE_USE_WEB
? ({ items: new Set([animatedComponent]) } as ViewRefSet<unknown>) // see makeViewsRefSet
: undefined;
const updaterFunction = () => {
'worklet';
const update = getInlinePropsUpdate(newInlineProps);
updateProps(shareableViewDescriptors, update, maybeViewRef);
};
this._inlineProps = newInlineProps;
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
this._inlinePropsMapperId = null;
if (Object.keys(newInlineProps).length) {
this._inlinePropsMapperId = startMapper(
updaterFunction,
Object.values(newInlineProps)
);
}
}
}
public detachInlineProps() {
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
}
}

View File

@@ -0,0 +1,155 @@
'use strict';
import { NativeEventEmitter, Platform, findNodeHandle } from 'react-native';
import type { NativeModule } from 'react-native';
import { shouldBeUseWeb } from '../reanimated2/PlatformChecker';
import type { StyleProps } from '../reanimated2';
import { runOnJS, runOnUIImmediately } from '../reanimated2/threads';
import type {
AnimatedComponentProps,
IAnimatedComponentInternal,
IJSPropsUpdater,
InitialComponentProps,
} from './commonTypes';
import NativeReanimatedModule from '../specs/NativeReanimatedModule';
interface ListenerData {
viewTag: number;
props: StyleProps;
}
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
class JSPropsUpdaterPaper implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map();
private _reanimatedEventEmitter: NativeEventEmitter;
constructor() {
this._reanimatedEventEmitter = new NativeEventEmitter(
// NativeEventEmitter only uses this parameter on iOS.
Platform.OS === 'ios'
? (NativeReanimatedModule as unknown as NativeModule)
: undefined
);
}
public addOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterPaper._tagToComponentMapping.set(viewTag, animatedComponent);
if (JSPropsUpdaterPaper._tagToComponentMapping.size === 1) {
const listener = (data: ListenerData) => {
const component = JSPropsUpdaterPaper._tagToComponentMapping.get(
data.viewTag
);
component?._updateFromNative(data.props);
};
this._reanimatedEventEmitter.addListener(
'onReanimatedPropsChange',
listener
);
}
}
public removeOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterPaper._tagToComponentMapping.delete(viewTag);
if (JSPropsUpdaterPaper._tagToComponentMapping.size === 0) {
this._reanimatedEventEmitter.removeAllListeners(
'onReanimatedPropsChange'
);
}
}
}
class JSPropsUpdaterFabric implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map();
private static isInitialized = false;
constructor() {
if (!JSPropsUpdaterFabric.isInitialized) {
const updater = (viewTag: number, props: unknown) => {
const component =
JSPropsUpdaterFabric._tagToComponentMapping.get(viewTag);
component?._updateFromNative(props);
};
runOnUIImmediately(() => {
'worklet';
global.updateJSProps = (viewTag: number, props: unknown) => {
runOnJS(updater)(viewTag, props);
};
})();
JSPropsUpdaterFabric.isInitialized = true;
}
}
public addOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
if (!JSPropsUpdaterFabric.isInitialized) {
return;
}
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterFabric._tagToComponentMapping.set(viewTag, animatedComponent);
}
public removeOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
if (!JSPropsUpdaterFabric.isInitialized) {
return;
}
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterFabric._tagToComponentMapping.delete(viewTag);
}
}
class JSPropsUpdaterWeb implements IJSPropsUpdater {
public addOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
public removeOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
}
type JSPropsUpdaterOptions =
| typeof JSPropsUpdaterWeb
| typeof JSPropsUpdaterFabric
| typeof JSPropsUpdaterPaper;
let JSPropsUpdater: JSPropsUpdaterOptions;
if (SHOULD_BE_USE_WEB) {
JSPropsUpdater = JSPropsUpdaterWeb;
} else if (global._IS_FABRIC) {
JSPropsUpdater = JSPropsUpdaterFabric;
} else {
JSPropsUpdater = JSPropsUpdaterPaper;
}
export default JSPropsUpdater;

View File

@@ -0,0 +1,26 @@
'use strict';
import type {
AnimatedComponentProps,
IAnimatedComponentInternal,
InitialComponentProps,
} from './commonTypes';
export default class JSPropsUpdaterWeb {
public addOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
public removeOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
}

View File

@@ -0,0 +1,117 @@
'use strict';
import { shallowEqual } from '../reanimated2/hook/utils';
import type { StyleProps } from '../reanimated2/commonTypes';
import { isSharedValue } from '../reanimated2/isSharedValue';
import { isChromeDebugger } from '../reanimated2/PlatformChecker';
import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler';
import { initialUpdaterRun } from '../reanimated2/animation';
import { hasInlineStyles, getInlineStyle } from './InlinePropManager';
import type {
AnimatedComponentProps,
AnimatedProps,
InitialComponentProps,
IAnimatedComponentInternal,
IPropsFilter,
} from './commonTypes';
import { flattenArray, has } from './utils';
import { StyleSheet } from 'react-native';
function dummyListener() {
// empty listener we use to assign to listener properties for which animated
// event is used.
}
export class PropsFilter implements IPropsFilter {
private _initialStyle = {};
private _previousProps: React.Component['props'] | null = null;
private _requiresNewInitials = true;
public filterNonAnimatedProps(
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
): Record<string, unknown> {
const inputProps =
component.props as AnimatedComponentProps<InitialComponentProps>;
this._maybePrepareForNewInitials(inputProps);
const props: Record<string, unknown> = {};
for (const key in inputProps) {
const value = inputProps[key];
if (key === 'style') {
const styleProp = inputProps.style;
const styles = flattenArray<StyleProps>(styleProp ?? []);
if (this._requiresNewInitials) {
this._initialStyle = {};
}
const processedStyle: StyleProps = styles.map((style) => {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
// TODO - refactor, since `viewsRef` is only present on Web
style.viewsRef?.add(component);
if (this._requiresNewInitials) {
this._initialStyle = {
...style.initial.value,
...this._initialStyle,
...initialUpdaterRun<StyleProps>(style.initial.updater),
};
}
return this._initialStyle;
} else if (hasInlineStyles(style)) {
return getInlineStyle(style, this._requiresNewInitials);
} else {
return style;
}
});
props[key] = StyleSheet.flatten(processedStyle);
} else if (key === 'animatedProps') {
const animatedProp = inputProps.animatedProps as Partial<
AnimatedComponentProps<AnimatedProps>
>;
if (animatedProp.initial !== undefined) {
Object.keys(animatedProp.initial.value).forEach((initialValueKey) => {
props[initialValueKey] =
animatedProp.initial?.value[initialValueKey];
// TODO - refacotr, since `viewsRef` is only present on Web
animatedProp.viewsRef?.add(component);
});
}
} else if (
has('workletEventHandler', value) &&
value.workletEventHandler instanceof WorkletEventHandler
) {
if (value.workletEventHandler.eventNames.length > 0) {
value.workletEventHandler.eventNames.forEach((eventName) => {
props[eventName] = has('listeners', value.workletEventHandler)
? (
value.workletEventHandler.listeners as Record<string, unknown>
)[eventName]
: dummyListener;
});
} else {
props[key] = dummyListener;
}
} else if (isSharedValue(value)) {
if (this._requiresNewInitials) {
props[key] = value.value;
}
} else if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) {
props[key] = value;
}
}
this._requiresNewInitials = false;
return props;
}
private _maybePrepareForNewInitials(
inputProps: AnimatedComponentProps<InitialComponentProps>
) {
if (this._previousProps && inputProps.style) {
this._requiresNewInitials = !shallowEqual(
this._previousProps,
inputProps
);
}
this._previousProps = inputProps;
}
}

View File

@@ -0,0 +1,116 @@
'use strict';
import type { Ref, Component } from 'react';
import type {
StyleProps,
BaseAnimationBuilder,
ILayoutAnimationBuilder,
EntryExitAnimationFunction,
SharedTransition,
SharedValue,
} from '../reanimated2';
import type {
ViewDescriptorsSet,
ViewRefSet,
} from '../reanimated2/ViewDescriptorsSet';
import type { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig';
import type { ShadowNodeWrapper } from '../reanimated2/commonTypes';
import type { ViewConfig } from '../ConfigHelper';
export interface AnimatedProps extends Record<string, unknown> {
viewDescriptors?: ViewDescriptorsSet;
viewsRef?: ViewRefSet<unknown>;
initial?: SharedValue<StyleProps>;
}
export interface ViewInfo {
viewTag: number | HTMLElement | null;
viewName: string | null;
shadowNodeWrapper: ShadowNodeWrapper | null;
viewConfig: ViewConfig;
}
export interface IInlinePropManager {
attachInlineProps(
animatedComponent: React.Component<unknown, unknown>,
viewInfo: ViewInfo
): void;
detachInlineProps(): void;
}
export interface IPropsFilter {
filterNonAnimatedProps: (
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
) => Record<string, unknown>;
}
export interface IJSPropsUpdater {
addOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): void;
removeOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): void;
}
export type LayoutAnimationStaticContext = {
presetName: string;
};
export type AnimatedComponentProps<P extends Record<string, unknown>> = P & {
forwardedRef?: Ref<Component>;
style?: NestedArray<StyleProps>;
animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
animatedStyle?: StyleProps;
layout?: (
| BaseAnimationBuilder
| ILayoutAnimationBuilder
| typeof BaseAnimationBuilder
) &
LayoutAnimationStaticContext;
entering?: (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
) &
LayoutAnimationStaticContext;
exiting?: (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
) &
LayoutAnimationStaticContext;
sharedTransitionTag?: string;
sharedTransitionStyle?: SharedTransition;
};
export interface AnimatedComponentRef extends Component {
setNativeProps?: (props: Record<string, unknown>) => void;
getScrollableNode?: () => AnimatedComponentRef;
getAnimatableRef?: () => AnimatedComponentRef;
}
export interface IAnimatedComponentInternal {
_styles: StyleProps[] | null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_viewTag: number;
_isFirstRender: boolean;
jestAnimatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
_sharedElementTransition: SharedTransition | null;
_jsPropsUpdater: IJSPropsUpdater;
_InlinePropManager: IInlinePropManager;
_PropsFilter: IPropsFilter;
_viewInfo?: ViewInfo;
context: React.ContextType<typeof SkipEnteringContext>;
}
export type NestedArray<T> = T | NestedArray<T>[];
export interface InitialComponentProps extends Record<string, unknown> {
ref?: Ref<Component>;
collapsable?: boolean;
}

View File

@@ -0,0 +1,631 @@
'use strict';
import type {
Component,
ComponentClass,
ComponentType,
FunctionComponent,
MutableRefObject,
} from 'react';
import React from 'react';
import { findNodeHandle, Platform } from 'react-native';
import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler';
import '../reanimated2/layoutReanimation/animationsManager';
import invariant from 'invariant';
import { adaptViewConfig } from '../ConfigHelper';
import { RNRenderer } from '../reanimated2/platform-specific/RNRenderer';
import { enableLayoutAnimations } from '../reanimated2/core';
import {
SharedTransition,
LayoutAnimationType,
} from '../reanimated2/layoutReanimation';
import type { StyleProps, ShadowNodeWrapper } from '../reanimated2/commonTypes';
import { getShadowNodeWrapperFromRef } from '../reanimated2/fabricUtils';
import { removeFromPropsRegistry } from '../reanimated2/PropsRegistry';
import { getReduceMotionFromConfig } from '../reanimated2/animation/util';
import { maybeBuild } from '../animationBuilder';
import { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig';
import type { AnimateProps } from '../reanimated2';
import JSPropsUpdater from './JSPropsUpdater';
import type {
AnimatedComponentProps,
AnimatedProps,
InitialComponentProps,
AnimatedComponentRef,
IAnimatedComponentInternal,
ViewInfo,
} from './commonTypes';
import { has, flattenArray } from './utils';
import setAndForwardRef from './setAndForwardRef';
import {
isFabric,
isJest,
isWeb,
shouldBeUseWeb,
} from '../reanimated2/PlatformChecker';
import { InlinePropManager } from './InlinePropManager';
import { PropsFilter } from './PropsFilter';
import {
startWebLayoutAnimation,
tryActivateLayoutTransition,
configureWebLayoutAnimations,
getReducedMotionFromConfig,
saveSnapshot,
} from '../reanimated2/layoutReanimation/web';
import { updateLayoutAnimations } from '../reanimated2/UpdateLayoutAnimations';
import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config';
import type { FlatList, FlatListProps } from 'react-native';
import { addHTMLMutationObserver } from '../reanimated2/layoutReanimation/web/domUtils';
import { getViewInfo } from './getViewInfo';
const IS_WEB = isWeb();
if (IS_WEB) {
configureWebLayoutAnimations();
}
function onlyAnimatedStyles(styles: StyleProps[]): StyleProps[] {
return styles.filter((style) => style?.viewDescriptors);
}
type Options<P> = {
setNativeProps: (ref: AnimatedComponentRef, props: P) => void;
};
/**
* Lets you create an Animated version of any React Native component.
*
* @param component - The component you want to make animatable.
* @returns A component that Reanimated is capable of animating.
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/createAnimatedComponent
*/
// Don't change the order of overloads, since such a change breaks current behavior
export function createAnimatedComponent<P extends object>(
component: FunctionComponent<P>,
options?: Options<P>
): FunctionComponent<AnimateProps<P>>;
export function createAnimatedComponent<P extends object>(
component: ComponentClass<P>,
options?: Options<P>
): ComponentClass<AnimateProps<P>>;
export function createAnimatedComponent<P extends object>(
// Actually ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P> but we need this overload too
// since some external components (like FastImage) are typed just as ComponentType
component: ComponentType<P>,
options?: Options<P>
): FunctionComponent<AnimateProps<P>> | ComponentClass<AnimateProps<P>>;
/**
* @deprecated Please use `Animated.FlatList` component instead of calling `Animated.createAnimatedComponent(FlatList)` manually.
*/
// @ts-ignore This is required to create this overload, since type of createAnimatedComponent is incorrect and doesn't include typeof FlatList
export function createAnimatedComponent(
component: typeof FlatList<unknown>,
options?: Options<any>
): ComponentClass<AnimateProps<FlatListProps<unknown>>>;
export function createAnimatedComponent(
Component: ComponentType<InitialComponentProps>,
options?: Options<InitialComponentProps>
): any {
invariant(
typeof Component !== 'function' ||
(Component.prototype && Component.prototype.isReactComponent),
`Looks like you're passing a function component \`${Component.name}\` to \`createAnimatedComponent\` function which supports only class components. Please wrap your function component with \`React.forwardRef()\` or use a class component instead.`
);
class AnimatedComponent
extends React.Component<AnimatedComponentProps<InitialComponentProps>>
implements IAnimatedComponentInternal
{
_styles: StyleProps[] | null = null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_viewTag = -1;
_isFirstRender = true;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
_component: AnimatedComponentRef | HTMLElement | null = null;
_sharedElementTransition: SharedTransition | null = null;
_jsPropsUpdater = new JSPropsUpdater();
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
_viewInfo?: ViewInfo;
static displayName: string;
static contextType = SkipEnteringContext;
context!: React.ContextType<typeof SkipEnteringContext>;
constructor(props: AnimatedComponentProps<InitialComponentProps>) {
super(props);
if (isJest()) {
this.jestAnimatedStyle = { value: {} };
}
}
componentDidMount() {
this._viewTag = this._getViewInfo().viewTag as number;
this._attachNativeEvents();
this._jsPropsUpdater.addOnJSPropsChangeListener(this);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
const layout = this.props.layout;
if (layout) {
this._configureLayoutTransition();
}
if (IS_WEB) {
if (this.props.exiting) {
saveSnapshot(this._component as HTMLElement);
}
if (
!this.props.entering ||
getReducedMotionFromConfig(this.props.entering as CustomConfig)
) {
this._isFirstRender = false;
return;
}
startWebLayoutAnimation(
this.props,
this._component as HTMLElement,
LayoutAnimationType.ENTERING
);
}
this._isFirstRender = false;
}
componentWillUnmount() {
this._detachNativeEvents();
this._jsPropsUpdater.removeOnJSPropsChangeListener(this);
this._detachStyles();
this._InlinePropManager.detachInlineProps();
if (this.props.sharedTransitionTag) {
this._configureSharedTransition(true);
}
this._sharedElementTransition?.unregisterTransition(this._viewTag, true);
const exiting = this.props.exiting;
if (
IS_WEB &&
this._component &&
this.props.exiting &&
!getReducedMotionFromConfig(this.props.exiting as CustomConfig)
) {
addHTMLMutationObserver();
startWebLayoutAnimation(
this.props,
this._component as HTMLElement,
LayoutAnimationType.EXITING
);
} else if (exiting) {
const reduceMotionInExiting =
'getReduceMotion' in exiting &&
typeof exiting.getReduceMotion === 'function'
? getReduceMotionFromConfig(exiting.getReduceMotion())
: getReduceMotionFromConfig();
if (!reduceMotionInExiting) {
updateLayoutAnimations(
this._viewTag,
LayoutAnimationType.EXITING,
maybeBuild(
exiting,
this.props?.style,
AnimatedComponent.displayName
)
);
}
}
}
_getEventViewRef() {
// Make sure to get the scrollable node for components that implement
// `ScrollResponder.Mixin`.
return (this._component as AnimatedComponentRef)?.getScrollableNode
? (this._component as AnimatedComponentRef).getScrollableNode?.()
: this._component;
}
_attachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
prop.workletEventHandler.registerForEvents(this._viewTag, key);
}
}
}
_detachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
prop.workletEventHandler.unregisterFromEvents(this._viewTag);
}
}
}
_detachStyles() {
if (IS_WEB && this._styles !== null) {
for (const style of this._styles) {
style.viewsRef.remove(this);
}
} else if (this._viewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
style.viewDescriptors.remove(this._viewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(this._viewTag);
}
if (isFabric()) {
removeFromPropsRegistry(this._viewTag);
}
}
}
_updateNativeEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
for (const key in prevProps) {
const prevProp = prevProps[key];
if (
has('workletEventHandler', prevProp) &&
prevProp.workletEventHandler instanceof WorkletEventHandler
) {
const newProp = this.props[key];
if (!newProp) {
// Prop got deleted
prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
} else if (
has('workletEventHandler', newProp) &&
newProp.workletEventHandler instanceof WorkletEventHandler &&
newProp.workletEventHandler !== prevProp.workletEventHandler
) {
// Prop got changed
prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
newProp.workletEventHandler.registerForEvents(this._viewTag);
}
}
}
for (const key in this.props) {
const newProp = this.props[key];
if (
has('workletEventHandler', newProp) &&
newProp.workletEventHandler instanceof WorkletEventHandler &&
!prevProps[key]
) {
// Prop got added
newProp.workletEventHandler.registerForEvents(this._viewTag);
}
}
}
_updateFromNative(props: StyleProps) {
if (options?.setNativeProps) {
options.setNativeProps(this._component as AnimatedComponentRef, props);
} else {
(this._component as AnimatedComponentRef)?.setNativeProps?.(props);
}
}
_getViewInfo(): ViewInfo {
if (this._viewInfo !== undefined) {
return this._viewInfo;
}
let viewTag: number | HTMLElement | null;
let viewName: string | null;
let shadowNodeWrapper: ShadowNodeWrapper | null = null;
let viewConfig;
// Component can specify ref which should be animated when animated version of the component is created.
// Otherwise, we animate the component itself.
const component = (this._component as AnimatedComponentRef)
?.getAnimatableRef
? (this._component as AnimatedComponentRef).getAnimatableRef?.()
: this;
if (IS_WEB) {
// At this point I assume that `_setComponentRef` was already called and `_component` is set.
// `this._component` on web represents HTMLElement of our component, that's why we use casting
viewTag = this._component as HTMLElement;
viewName = null;
shadowNodeWrapper = null;
viewConfig = null;
} else {
// hostInstance can be null for a component that doesn't render anything (render function returns null). Example: svg Stop: https://github.com/react-native-svg/react-native-svg/blob/develop/src/elements/Stop.tsx
const hostInstance = RNRenderer.findHostInstance_DEPRECATED(component);
if (!hostInstance) {
throw new Error(
'[Reanimated] Cannot find host instance for this component. Maybe it renders nothing?'
);
}
const viewInfo = getViewInfo(hostInstance);
viewTag = viewInfo.viewTag;
viewName = viewInfo.viewName;
viewConfig = viewInfo.viewConfig;
shadowNodeWrapper = isFabric()
? getShadowNodeWrapperFromRef(this)
: null;
}
this._viewInfo = { viewTag, viewName, shadowNodeWrapper, viewConfig };
return this._viewInfo;
}
_attachAnimatedStyles() {
const styles = this.props.style
? onlyAnimatedStyles(flattenArray<StyleProps>(this.props.style))
: [];
const prevStyles = this._styles;
this._styles = styles;
const prevAnimatedProps = this._animatedProps;
this._animatedProps = this.props.animatedProps;
const { viewTag, viewName, shadowNodeWrapper, viewConfig } =
this._getViewInfo();
// update UI props whitelist for this view
const hasReanimated2Props =
this.props.animatedProps?.viewDescriptors || styles.length;
if (hasReanimated2Props && viewConfig) {
adaptViewConfig(viewConfig);
}
this._viewTag = viewTag as number;
// remove old styles
if (prevStyles) {
// in most of the cases, views have only a single animated style and it remains unchanged
const hasOneSameStyle =
styles.length === 1 &&
prevStyles.length === 1 &&
styles[0] === prevStyles[0];
if (!hasOneSameStyle) {
// otherwise, remove each style that is not present in new styles
for (const prevStyle of prevStyles) {
const isPresent = styles.some((style) => style === prevStyle);
if (!isPresent) {
prevStyle.viewDescriptors.remove(viewTag);
}
}
}
}
styles.forEach((style) => {
style.viewDescriptors.add({
tag: viewTag,
name: viewName,
shadowNodeWrapper,
});
if (isJest()) {
/**
* We need to connect Jest's TestObject instance whose contains just props object
* with the updateProps() function where we update the properties of the component.
* We can't update props object directly because TestObject contains a copy of props - look at render function:
* const props = this._filterNonAnimatedProps(this.props);
*/
this.jestAnimatedStyle.value = {
...this.jestAnimatedStyle.value,
...style.initial.value,
};
style.jestAnimatedStyle.current = this.jestAnimatedStyle;
}
});
// detach old animatedProps
if (prevAnimatedProps && prevAnimatedProps !== this.props.animatedProps) {
prevAnimatedProps.viewDescriptors!.remove(viewTag as number);
}
// attach animatedProps property
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.add({
tag: viewTag as number,
name: viewName!,
shadowNodeWrapper: shadowNodeWrapper!,
});
}
}
componentDidUpdate(
prevProps: AnimatedComponentProps<InitialComponentProps>,
_prevState: Readonly<unknown>,
// This type comes straight from React
// eslint-disable-next-line @typescript-eslint/no-explicit-any
snapshot: DOMRect | null
) {
const layout = this.props.layout;
const oldLayout = prevProps.layout;
if (layout !== oldLayout) {
this._configureLayoutTransition();
}
if (
this.props.sharedTransitionTag !== undefined ||
prevProps.sharedTransitionTag !== undefined
) {
this._configureSharedTransition();
}
this._updateNativeEvents(prevProps);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (IS_WEB && this.props.exiting) {
saveSnapshot(this._component as HTMLElement);
}
// Snapshot won't be undefined because it comes from getSnapshotBeforeUpdate method
if (
IS_WEB &&
snapshot !== null &&
this.props.layout &&
!getReducedMotionFromConfig(this.props.layout as CustomConfig)
) {
tryActivateLayoutTransition(
this.props,
this._component as HTMLElement,
snapshot
);
}
}
_configureLayoutTransition() {
const layout = this.props.layout
? maybeBuild(
this.props.layout,
undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */,
AnimatedComponent.displayName
)
: undefined;
updateLayoutAnimations(this._viewTag, LayoutAnimationType.LAYOUT, layout);
}
_configureSharedTransition(isUnmounting = false) {
if (IS_WEB) {
return;
}
const { sharedTransitionTag } = this.props;
if (!sharedTransitionTag) {
this._sharedElementTransition?.unregisterTransition(
this._viewTag,
isUnmounting
);
this._sharedElementTransition = null;
return;
}
const sharedElementTransition =
this.props.sharedTransitionStyle ??
this._sharedElementTransition ??
new SharedTransition();
sharedElementTransition.registerTransition(
this._viewTag,
sharedTransitionTag,
isUnmounting
);
this._sharedElementTransition = sharedElementTransition;
}
_setComponentRef = setAndForwardRef<Component | HTMLElement>({
getForwardedRef: () =>
this.props.forwardedRef as MutableRefObject<
Component<Record<string, unknown>, Record<string, unknown>, unknown>
>,
setLocalRef: (ref) => {
// TODO update config
const tag = IS_WEB
? (ref as HTMLElement)
: findNodeHandle(ref as Component);
this._viewTag = tag as number;
const { layout, entering, exiting, sharedTransitionTag } = this.props;
if (
(layout || entering || exiting || sharedTransitionTag) &&
tag != null
) {
if (!shouldBeUseWeb()) {
enableLayoutAnimations(true, false);
}
if (sharedTransitionTag) {
this._configureSharedTransition();
}
const skipEntering = this.context?.current;
if (entering && !skipEntering) {
updateLayoutAnimations(
tag as number,
LayoutAnimationType.ENTERING,
maybeBuild(
entering,
this.props?.style,
AnimatedComponent.displayName
)
);
}
}
if (ref !== this._component) {
this._component = ref;
}
},
});
// This is a component lifecycle method from React, therefore we are not calling it directly.
// It is called before the component gets rerendered. This way we can access components' position before it changed
// and later on, in componentDidUpdate, calculate translation for layout transition.
getSnapshotBeforeUpdate() {
if (
IS_WEB &&
(this._component as HTMLElement)?.getBoundingClientRect !== undefined
) {
return (this._component as HTMLElement).getBoundingClientRect();
}
return null;
}
render() {
const filteredProps = this._PropsFilter.filterNonAnimatedProps(this);
if (isJest()) {
filteredProps.jestAnimatedStyle = this.jestAnimatedStyle;
}
// Layout animations on web are set inside `componentDidMount` method, which is called after first render.
// Because of that we can encounter a situation in which component is visible for a short amount of time, and later on animation triggers.
// I've tested that on various browsers and devices and it did not happen to me. To be sure that it won't happen to someone else,
// I've decided to hide component at first render. Its visibility is reset in `componentDidMount`.
if (
this._isFirstRender &&
IS_WEB &&
filteredProps.entering &&
!getReducedMotionFromConfig(filteredProps.entering as CustomConfig)
) {
filteredProps.style = {
...(filteredProps.style ?? {}),
visibility: 'hidden', // Hide component until `componentDidMount` triggers
};
}
const platformProps = Platform.select({
web: {},
default: { collapsable: false },
});
return (
<Component
{...filteredProps}
// Casting is used here, because ref can be null - in that case it cannot be assigned to HTMLElement.
// After spending some time trying to figure out what to do with this problem, we decided to leave it this way
ref={this._setComponentRef as (ref: Component) => void}
{...platformProps}
/>
);
}
}
AnimatedComponent.displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
return React.forwardRef<Component>((props, ref) => {
return (
<AnimatedComponent
{...props}
{...(ref === null ? null : { forwardedRef: ref })}
/>
);
});
}

View File

@@ -0,0 +1,39 @@
'use strict';
/* eslint-disable @typescript-eslint/no-explicit-any */
// This is a makeshift solution to handle both 0.73 and 0.74 versions of React Native.
export let getViewInfo = (element: any) => {
if (element._nativeTag !== undefined && element.__nativeTag !== null) {
getViewInfo = getViewInfo73;
return getViewInfo73(element);
} else if (
element.__nativeTag !== undefined &&
element.__nativeTag !== null
) {
getViewInfo = getViewInfoLatest;
return getViewInfoLatest(element);
}
return getViewInfo73(element);
};
function getViewInfo73(element: any) {
return {
// we can access view tag in the same way it's accessed here https://github.com/facebook/react/blob/e3f4eb7272d4ca0ee49f27577156b57eeb07cf73/packages/react-native-renderer/src/ReactFabric.js#L146
viewName: element?.viewConfig?.uiViewClassName,
/**
* RN uses viewConfig for components for storing different properties of the component(example: https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js#L24).
* The name we're looking for is in the field named uiViewClassName.
*/
viewTag: element?._nativeTag,
viewConfig: element?.viewConfig,
};
}
function getViewInfoLatest(element: any) {
return {
viewName: element?._viewConfig?.uiViewClassName,
viewTag: element?.__nativeTag,
viewConfig: element?._viewConfig,
};
}

View File

@@ -0,0 +1,2 @@
'use strict';
export { createAnimatedComponent } from './createAnimatedComponent';

View File

@@ -0,0 +1,66 @@
'use strict';
/**
* imported from react-native
*/
import type { MutableRefObject } from 'react';
/* eslint-disable */
/**
* This is a helper function for when a component needs to be able to forward a ref
* to a child component, but still needs to have access to that component as part of
* its implementation.
*
* Its main use case is in wrappers for native components.
*
* Usage:
*
* class MyView extends React.Component {
* _nativeRef = null;
*
* _setNativeRef = setAndForwardRef({
* getForwardedRef: () => this.props.forwardedRef,
* setLocalRef: ref => {
* this._nativeRef = ref;
* },
* });
*
* render() {
* return <View ref={this._setNativeRef} />;
* }
* }
*
* const MyViewWithRef = React.forwardRef((props, ref) => (
* <MyView {...props} forwardedRef={ref} />
* ));
*
* module.exports = MyViewWithRef;
*/
/* eslint-enable */
type ForwardedRef<T> = () => MutableRefObject<T> | ((ref: T) => void);
function setAndForwardRef<T>({
getForwardedRef,
setLocalRef,
}: {
getForwardedRef: ForwardedRef<T>;
setLocalRef: (ref: T) => void;
}): (ref: T) => void {
return function forwardRef(ref: T) {
const forwardedRef = getForwardedRef();
setLocalRef(ref);
// Forward to user ref prop (if one has been specified)
if (typeof forwardedRef === 'function') {
// Handle function-based refs. String-based refs are handled as functions.
forwardedRef(ref);
} else if (typeof forwardedRef === 'object' && forwardedRef != null) {
// Handle createRef-based refs
forwardedRef.current = ref;
}
};
}
export default setAndForwardRef;

View File

@@ -0,0 +1,35 @@
'use strict';
import type { NestedArray } from './commonTypes';
export function flattenArray<T>(array: NestedArray<T>): T[] {
if (!Array.isArray(array)) {
return [array];
}
const resultArr: T[] = [];
const _flattenArray = (arr: NestedArray<T>[]): void => {
arr.forEach((item) => {
if (Array.isArray(item)) {
_flattenArray(item);
} else {
resultArr.push(item);
}
});
};
_flattenArray(array);
return resultArr;
}
export const has = <K extends string>(
key: K,
x: unknown
): x is { [key in K]: unknown } => {
if (typeof x === 'function' || typeof x === 'object') {
if (x === null || x === undefined) {
return false;
} else {
return key in x;
}
}
return false;
};