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,126 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedNode from '../nodes/AnimatedNode';
import type AnimatedValue from '../nodes/AnimatedValue';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedProps from '../nodes/AnimatedProps';
export type EndResult = {finished: boolean, value?: number, ...};
export type EndCallback = (result: EndResult) => void;
export type AnimationConfig = {
isInteraction?: boolean,
useNativeDriver: boolean,
platformConfig?: PlatformConfig,
onComplete?: ?EndCallback,
iterations?: number,
};
let startNativeAnimationNextId = 1;
// Important note: start() and stop() will only be called at most once.
// Once an animation has been stopped or finished its course, it will
// not be reused.
export default class Animation {
__active: boolean;
__isInteraction: boolean;
__onEnd: ?EndCallback;
__iterations: number;
_nativeId: number;
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {}
stop(): void {
if (this._nativeId) {
NativeAnimatedHelper.API.stopAnimation(this._nativeId);
}
}
__getNativeAnimationConfig(): any {
// Subclasses that have corresponding animation implementation done in native
// should override this method
throw new Error('This animation type cannot be offloaded to native');
}
// Helper function for subclasses to make sure onEnd is only called once.
__debouncedOnEnd(result: EndResult): void {
const onEnd = this.__onEnd;
this.__onEnd = null;
onEnd && onEnd(result);
}
__findAnimatedPropsNodes(node: AnimatedNode): Array<AnimatedProps> {
const result = [];
if (node instanceof AnimatedProps) {
result.push(node);
return result;
}
for (const child of node.__getChildren()) {
result.push(...this.__findAnimatedPropsNodes(child));
}
return result;
}
__startNativeAnimation(animatedValue: AnimatedValue): void {
const startNativeAnimationWaitId = `${startNativeAnimationNextId}:startAnimation`;
startNativeAnimationNextId += 1;
NativeAnimatedHelper.API.setWaitingForIdentifier(
startNativeAnimationWaitId,
);
try {
const config = this.__getNativeAnimationConfig();
animatedValue.__makeNative(config.platformConfig);
this._nativeId = NativeAnimatedHelper.generateNewAnimationId();
NativeAnimatedHelper.API.startAnimatingNode(
this._nativeId,
animatedValue.__getNativeTag(),
config,
result => {
this.__debouncedOnEnd(result);
// When using natively driven animations, once the animation completes,
// we need to ensure that the JS side nodes are synced with the updated
// values.
const {value} = result;
if (value != null) {
animatedValue.__onAnimatedValueUpdateReceived(value);
// Once the JS side node is synced with the updated values, trigger an
// update on the AnimatedProps nodes to call any registered callbacks.
this.__findAnimatedPropsNodes(animatedValue).forEach(node =>
node.update(),
);
}
},
);
} catch (e) {
throw e;
} finally {
NativeAnimatedHelper.API.unsetWaitingForIdentifier(
startNativeAnimationWaitId,
);
}
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedValue from '../nodes/AnimatedValue';
import type {AnimationConfig, EndCallback} from './Animation';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import Animation from './Animation';
export type DecayAnimationConfig = {
...AnimationConfig,
velocity:
| number
| {
x: number,
y: number,
...
},
deceleration?: number,
};
export type DecayAnimationConfigSingle = {
...AnimationConfig,
velocity: number,
deceleration?: number,
};
export default class DecayAnimation extends Animation {
_startTime: number;
_lastValue: number;
_fromValue: number;
_deceleration: number;
_velocity: number;
_onUpdate: (value: number) => void;
_animationFrame: any;
_useNativeDriver: boolean;
_platformConfig: ?PlatformConfig;
constructor(config: DecayAnimationConfigSingle) {
super();
this._deceleration = config.deceleration ?? 0.998;
this._velocity = config.velocity;
this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
this._platformConfig = config.platformConfig;
this.__isInteraction = config.isInteraction ?? !this._useNativeDriver;
this.__iterations = config.iterations ?? 1;
}
__getNativeAnimationConfig(): {|
deceleration: number,
iterations: number,
platformConfig: ?PlatformConfig,
type: $TEMPORARY$string<'decay'>,
velocity: number,
|} {
return {
type: 'decay',
deceleration: this._deceleration,
velocity: this._velocity,
iterations: this.__iterations,
platformConfig: this._platformConfig,
};
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {
this.__active = true;
this._lastValue = fromValue;
this._fromValue = fromValue;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
this._startTime = Date.now();
if (!this._useNativeDriver && animatedValue.__isNative === true) {
throw new Error(
'Attempting to run JS driven animation on animated node ' +
'that has been moved to "native" earlier by starting an ' +
'animation with `useNativeDriver: true`',
);
}
if (this._useNativeDriver) {
this.__startNativeAnimation(animatedValue);
} else {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
}
onUpdate(): void {
const now = Date.now();
const value =
this._fromValue +
(this._velocity / (1 - this._deceleration)) *
(1 - Math.exp(-(1 - this._deceleration) * (now - this._startTime)));
this._onUpdate(value);
if (Math.abs(this._lastValue - value) < 0.1) {
this.__debouncedOnEnd({finished: true});
return;
}
this._lastValue = value;
if (this.__active) {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
}
stop(): void {
super.stop();
this.__active = false;
global.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}

View File

@@ -0,0 +1,378 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedInterpolation from '../nodes/AnimatedInterpolation';
import type AnimatedValue from '../nodes/AnimatedValue';
import type AnimatedValueXY from '../nodes/AnimatedValueXY';
import type {AnimationConfig, EndCallback} from './Animation';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedColor from '../nodes/AnimatedColor';
import * as SpringConfig from '../SpringConfig';
import Animation from './Animation';
import invariant from 'invariant';
export type SpringAnimationConfig = {
...AnimationConfig,
toValue:
| number
| AnimatedValue
| {
x: number,
y: number,
...
}
| AnimatedValueXY
| {
r: number,
g: number,
b: number,
a: number,
...
}
| AnimatedColor
| AnimatedInterpolation<number>,
overshootClamping?: boolean,
restDisplacementThreshold?: number,
restSpeedThreshold?: number,
velocity?:
| number
| {
x: number,
y: number,
...
},
bounciness?: number,
speed?: number,
tension?: number,
friction?: number,
stiffness?: number,
damping?: number,
mass?: number,
delay?: number,
};
export type SpringAnimationConfigSingle = {
...AnimationConfig,
toValue: number,
overshootClamping?: boolean,
restDisplacementThreshold?: number,
restSpeedThreshold?: number,
velocity?: number,
bounciness?: number,
speed?: number,
tension?: number,
friction?: number,
stiffness?: number,
damping?: number,
mass?: number,
delay?: number,
};
export default class SpringAnimation extends Animation {
_overshootClamping: boolean;
_restDisplacementThreshold: number;
_restSpeedThreshold: number;
_lastVelocity: number;
_startPosition: number;
_lastPosition: number;
_fromValue: number;
_toValue: number;
_stiffness: number;
_damping: number;
_mass: number;
_initialVelocity: number;
_delay: number;
_timeout: any;
_startTime: number;
_lastTime: number;
_frameTime: number;
_onUpdate: (value: number) => void;
_animationFrame: any;
_useNativeDriver: boolean;
_platformConfig: ?PlatformConfig;
constructor(config: SpringAnimationConfigSingle) {
super();
this._overshootClamping = config.overshootClamping ?? false;
this._restDisplacementThreshold = config.restDisplacementThreshold ?? 0.001;
this._restSpeedThreshold = config.restSpeedThreshold ?? 0.001;
this._initialVelocity = config.velocity ?? 0;
this._lastVelocity = config.velocity ?? 0;
this._toValue = config.toValue;
this._delay = config.delay ?? 0;
this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
this._platformConfig = config.platformConfig;
this.__isInteraction = config.isInteraction ?? !this._useNativeDriver;
this.__iterations = config.iterations ?? 1;
if (
config.stiffness !== undefined ||
config.damping !== undefined ||
config.mass !== undefined
) {
invariant(
config.bounciness === undefined &&
config.speed === undefined &&
config.tension === undefined &&
config.friction === undefined,
'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one',
);
this._stiffness = config.stiffness ?? 100;
this._damping = config.damping ?? 10;
this._mass = config.mass ?? 1;
} else if (config.bounciness !== undefined || config.speed !== undefined) {
// Convert the origami bounciness/speed values to stiffness/damping
// We assume mass is 1.
invariant(
config.tension === undefined &&
config.friction === undefined &&
config.stiffness === undefined &&
config.damping === undefined &&
config.mass === undefined,
'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one',
);
const springConfig = SpringConfig.fromBouncinessAndSpeed(
config.bounciness ?? 8,
config.speed ?? 12,
);
this._stiffness = springConfig.stiffness;
this._damping = springConfig.damping;
this._mass = 1;
} else {
// Convert the origami tension/friction values to stiffness/damping
// We assume mass is 1.
const springConfig = SpringConfig.fromOrigamiTensionAndFriction(
config.tension ?? 40,
config.friction ?? 7,
);
this._stiffness = springConfig.stiffness;
this._damping = springConfig.damping;
this._mass = 1;
}
invariant(this._stiffness > 0, 'Stiffness value must be greater than 0');
invariant(this._damping > 0, 'Damping value must be greater than 0');
invariant(this._mass > 0, 'Mass value must be greater than 0');
}
__getNativeAnimationConfig(): {|
damping: number,
initialVelocity: number,
iterations: number,
mass: number,
platformConfig: ?PlatformConfig,
overshootClamping: boolean,
restDisplacementThreshold: number,
restSpeedThreshold: number,
stiffness: number,
toValue: any,
type: $TEMPORARY$string<'spring'>,
|} {
return {
type: 'spring',
overshootClamping: this._overshootClamping,
restDisplacementThreshold: this._restDisplacementThreshold,
restSpeedThreshold: this._restSpeedThreshold,
stiffness: this._stiffness,
damping: this._damping,
mass: this._mass,
initialVelocity: this._initialVelocity ?? this._lastVelocity,
toValue: this._toValue,
iterations: this.__iterations,
platformConfig: this._platformConfig,
};
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {
this.__active = true;
this._startPosition = fromValue;
this._lastPosition = this._startPosition;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
this._lastTime = Date.now();
this._frameTime = 0.0;
if (previousAnimation instanceof SpringAnimation) {
const internalState = previousAnimation.getInternalState();
this._lastPosition = internalState.lastPosition;
this._lastVelocity = internalState.lastVelocity;
// Set the initial velocity to the last velocity
this._initialVelocity = this._lastVelocity;
this._lastTime = internalState.lastTime;
}
const start = () => {
if (!this._useNativeDriver && animatedValue.__isNative === true) {
throw new Error(
'Attempting to run JS driven animation on animated node ' +
'that has been moved to "native" earlier by starting an ' +
'animation with `useNativeDriver: true`',
);
}
if (this._useNativeDriver) {
this.__startNativeAnimation(animatedValue);
} else {
this.onUpdate();
}
};
// If this._delay is more than 0, we start after the timeout.
if (this._delay) {
this._timeout = setTimeout(start, this._delay);
} else {
start();
}
}
getInternalState(): Object {
return {
lastPosition: this._lastPosition,
lastVelocity: this._lastVelocity,
lastTime: this._lastTime,
};
}
/**
* This spring model is based off of a damped harmonic oscillator
* (https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
*
* We use the closed form of the second order differential equation:
*
* x'' + (2ζ⍵_0)x' + ⍵^2x = 0
*
* where
* ⍵_0 = √(k / m) (undamped angular frequency of the oscillator),
* ζ = c / 2√mk (damping ratio),
* c = damping constant
* k = stiffness
* m = mass
*
* The derivation of the closed form is described in detail here:
* http://planetmath.org/sites/default/files/texpdf/39745.pdf
*
* This algorithm happens to match the algorithm used by CASpringAnimation,
* a QuartzCore (iOS) API that creates spring animations.
*/
onUpdate(): void {
// If for some reason we lost a lot of frames (e.g. process large payload or
// stopped in the debugger), we only advance by 4 frames worth of
// computation and will continue on the next frame. It's better to have it
// running at faster speed than jumping to the end.
const MAX_STEPS = 64;
let now = Date.now();
if (now > this._lastTime + MAX_STEPS) {
now = this._lastTime + MAX_STEPS;
}
const deltaTime = (now - this._lastTime) / 1000;
this._frameTime += deltaTime;
const c: number = this._damping;
const m: number = this._mass;
const k: number = this._stiffness;
const v0: number = -this._initialVelocity;
const zeta = c / (2 * Math.sqrt(k * m)); // damping ratio
const omega0 = Math.sqrt(k / m); // undamped angular frequency of the oscillator (rad/ms)
const omega1 = omega0 * Math.sqrt(1.0 - zeta * zeta); // exponential decay
const x0 = this._toValue - this._startPosition; // calculate the oscillation from x0 = 1 to x = 0
let position = 0.0;
let velocity = 0.0;
const t = this._frameTime;
if (zeta < 1) {
// Under damped
const envelope = Math.exp(-zeta * omega0 * t);
position =
this._toValue -
envelope *
(((v0 + zeta * omega0 * x0) / omega1) * Math.sin(omega1 * t) +
x0 * Math.cos(omega1 * t));
// This looks crazy -- it's actually just the derivative of the
// oscillation function
velocity =
zeta *
omega0 *
envelope *
((Math.sin(omega1 * t) * (v0 + zeta * omega0 * x0)) / omega1 +
x0 * Math.cos(omega1 * t)) -
envelope *
(Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) -
omega1 * x0 * Math.sin(omega1 * t));
} else {
// Critically damped
const envelope = Math.exp(-omega0 * t);
position = this._toValue - envelope * (x0 + (v0 + omega0 * x0) * t);
velocity =
envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0));
}
this._lastTime = now;
this._lastPosition = position;
this._lastVelocity = velocity;
this._onUpdate(position);
if (!this.__active) {
// a listener might have stopped us in _onUpdate
return;
}
// Conditions for stopping the spring animation
let isOvershooting = false;
if (this._overshootClamping && this._stiffness !== 0) {
if (this._startPosition < this._toValue) {
isOvershooting = position > this._toValue;
} else {
isOvershooting = position < this._toValue;
}
}
const isVelocity = Math.abs(velocity) <= this._restSpeedThreshold;
let isDisplacement = true;
if (this._stiffness !== 0) {
isDisplacement =
Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
}
if (isOvershooting || (isVelocity && isDisplacement)) {
if (this._stiffness !== 0) {
// Ensure that we end up with a round value
this._lastPosition = this._toValue;
this._lastVelocity = 0;
this._onUpdate(this._toValue);
}
this.__debouncedOnEnd({finished: true});
return;
}
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
stop(): void {
super.stop();
this.__active = false;
clearTimeout(this._timeout);
global.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}

View File

@@ -0,0 +1,180 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {RgbaValue} from '../nodes/AnimatedColor';
import type AnimatedInterpolation from '../nodes/AnimatedInterpolation';
import type AnimatedValue from '../nodes/AnimatedValue';
import type AnimatedValueXY from '../nodes/AnimatedValueXY';
import type {AnimationConfig, EndCallback} from './Animation';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedColor from '../nodes/AnimatedColor';
import Animation from './Animation';
export type TimingAnimationConfig = $ReadOnly<{
...AnimationConfig,
toValue:
| number
| AnimatedValue
| {
x: number,
y: number,
...
}
| AnimatedValueXY
| RgbaValue
| AnimatedColor
| AnimatedInterpolation<number>,
easing?: (value: number) => number,
duration?: number,
delay?: number,
}>;
export type TimingAnimationConfigSingle = $ReadOnly<{
...AnimationConfig,
toValue: number,
easing?: (value: number) => number,
duration?: number,
delay?: number,
}>;
let _easeInOut;
function easeInOut() {
if (!_easeInOut) {
const Easing = require('../Easing').default;
_easeInOut = Easing.inOut(Easing.ease);
}
return _easeInOut;
}
export default class TimingAnimation extends Animation {
_startTime: number;
_fromValue: number;
_toValue: number;
_duration: number;
_delay: number;
_easing: (value: number) => number;
_onUpdate: (value: number) => void;
_animationFrame: any;
_timeout: any;
_useNativeDriver: boolean;
_platformConfig: ?PlatformConfig;
constructor(config: TimingAnimationConfigSingle) {
super();
this._toValue = config.toValue;
this._easing = config.easing ?? easeInOut();
this._duration = config.duration ?? 500;
this._delay = config.delay ?? 0;
this.__iterations = config.iterations ?? 1;
this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
this._platformConfig = config.platformConfig;
this.__isInteraction = config.isInteraction ?? !this._useNativeDriver;
}
__getNativeAnimationConfig(): any {
const frameDuration = 1000.0 / 60.0;
const frames = [];
const numFrames = Math.round(this._duration / frameDuration);
for (let frame = 0; frame < numFrames; frame++) {
frames.push(this._easing(frame / numFrames));
}
frames.push(this._easing(1));
return {
type: 'frames',
frames,
toValue: this._toValue,
iterations: this.__iterations,
platformConfig: this._platformConfig,
};
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {
this.__active = true;
this._fromValue = fromValue;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
const start = () => {
if (!this._useNativeDriver && animatedValue.__isNative === true) {
throw new Error(
'Attempting to run JS driven animation on animated node ' +
'that has been moved to "native" earlier by starting an ' +
'animation with `useNativeDriver: true`',
);
}
// Animations that sometimes have 0 duration and sometimes do not
// still need to use the native driver when duration is 0 so as to
// not cause intermixed JS and native animations.
if (this._duration === 0 && !this._useNativeDriver) {
this._onUpdate(this._toValue);
this.__debouncedOnEnd({finished: true});
} else {
this._startTime = Date.now();
if (this._useNativeDriver) {
this.__startNativeAnimation(animatedValue);
} else {
this._animationFrame = requestAnimationFrame(
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this.onUpdate.bind(this),
);
}
}
};
if (this._delay) {
this._timeout = setTimeout(start, this._delay);
} else {
start();
}
}
onUpdate(): void {
const now = Date.now();
if (now >= this._startTime + this._duration) {
if (this._duration === 0) {
this._onUpdate(this._toValue);
} else {
this._onUpdate(
this._fromValue + this._easing(1) * (this._toValue - this._fromValue),
);
}
this.__debouncedOnEnd({finished: true});
return;
}
this._onUpdate(
this._fromValue +
this._easing((now - this._startTime) / this._duration) *
(this._toValue - this._fromValue),
);
if (this.__active) {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
}
stop(): void {
super.stop();
this.__active = false;
clearTimeout(this._timeout);
global.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}