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,21 @@
/**
* 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.
*
* @format
*/
'use strict';
const MockNativeMethods = {
measure: jest.fn(),
measureInWindow: jest.fn(),
measureLayout: jest.fn(),
setNativeProps: jest.fn(),
focus: jest.fn(),
blur: jest.fn(),
};
module.exports = MockNativeMethods;

View File

@@ -0,0 +1,13 @@
/**
* 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.
*
* @format
* @flow strict
*/
'use strict';
module.exports = {};

View File

@@ -0,0 +1,18 @@
/**
* 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 strict
* @format
* @oncall react_native
*/
import NativeExceptionsManager from '../../Libraries/Core/NativeExceptionsManager';
test('NativeExceptionsManager is a mock', () => {
expect(jest.isMockFunction(NativeExceptionsManager.reportException)).toBe(
true,
);
});

View File

@@ -0,0 +1,32 @@
/**
* 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.
*
* @format
*/
'use strict';
/* eslint-env node */
const createCacheKeyFunction =
require('@jest/create-cache-key-function').default;
const path = require('path');
module.exports = {
// Mocks asset requires to return the filename. Makes it possible to test that
// the correct images are loaded for components. Essentially
// require('img1.png') becomes `Object { "testUri": 'path/to/img1.png' }` in
// the Jest snapshot.
process: (_, filename) => ({
code: `module.exports = {
testUri:
${JSON.stringify(
path.relative(__dirname, filename).replace(/\\/g, '/'),
)}
};`,
}),
getCacheKey: createCacheKeyFunction([__filename]),
};

View File

@@ -0,0 +1,28 @@
/**
* 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.
*
* @format
*/
// Global setup for tests local to the react-native repo. This setup is not
// included in the react-native Jest preset.
'use strict';
require('./setup');
const consoleError = console.error;
const consoleWarn = console.warn;
console.error = (...args) => {
consoleError(...args);
throw new Error('console.error() was called (see error above)');
};
console.warn = (...args) => {
consoleWarn(...args);
throw new Error('console.warn() was called (see warning above)');
};

View File

@@ -0,0 +1,64 @@
/**
* 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.
*
* @format
*/
'use strict';
module.exports = (moduleName, instanceMethods, isESModule) => {
const RealComponent = isESModule
? jest.requireActual(moduleName).default
: jest.requireActual(moduleName);
const React = require('react');
const SuperClass =
typeof RealComponent === 'function' ? RealComponent : React.Component;
const name =
RealComponent.displayName ||
RealComponent.name ||
(RealComponent.render // handle React.forwardRef
? RealComponent.render.displayName || RealComponent.render.name
: 'Unknown');
const nameWithoutPrefix = name.replace(/^(RCT|RK)/, '');
const Component = class extends SuperClass {
static displayName = 'Component';
render() {
const props = Object.assign({}, RealComponent.defaultProps);
if (this.props) {
Object.keys(this.props).forEach(prop => {
// We can't just assign props on top of defaultProps
// because React treats undefined as special and different from null.
// If a prop is specified but set to undefined it is ignored and the
// default prop is used instead. If it is set to null, then the
// null value overwrites the default value.
if (this.props[prop] !== undefined) {
props[prop] = this.props[prop];
}
});
}
return React.createElement(nameWithoutPrefix, props, this.props.children);
}
};
Component.displayName = nameWithoutPrefix;
Object.keys(RealComponent).forEach(classStatic => {
Component[classStatic] = RealComponent[classStatic];
});
if (instanceMethods != null) {
Object.assign(Component.prototype, instanceMethods);
}
return Component;
};

View File

@@ -0,0 +1,34 @@
/**
* 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.
*
* @format
* @flow strict-local
*/
/* eslint-env jest */
'use strict';
import typeof Modal from '../Libraries/Modal/Modal';
const React = require('react');
function mockModal(BaseComponent: $FlowFixMe) {
class ModalMock extends BaseComponent {
render(): React.Element<Modal> | null {
if (this.props.visible === false) {
return null;
}
return (
<BaseComponent {...this.props}>{this.props.children}</BaseComponent>
);
}
}
return ModalMock;
}
module.exports = (mockModal: $FlowFixMe);

View File

@@ -0,0 +1,40 @@
/**
* 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.
*
* @format
*/
'use strict';
const React = require('react');
let nativeTag = 1;
export default viewName => {
const Component = class extends React.Component {
_nativeTag = nativeTag++;
render() {
return React.createElement(viewName, this.props, this.props.children);
}
// The methods that exist on host components
blur = jest.fn();
focus = jest.fn();
measure = jest.fn();
measureInWindow = jest.fn();
measureLayout = jest.fn();
setNativeProps = jest.fn();
};
if (viewName === 'RCTView') {
Component.displayName = 'View';
} else {
Component.displayName = viewName;
}
return Component;
};

View File

@@ -0,0 +1,35 @@
/**
* 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.
*
* @format
* @flow strict-local
*/
/* eslint-env jest */
'use strict';
const View = require('../Libraries/Components/View/View');
const requireNativeComponent =
require('../Libraries/ReactNative/requireNativeComponent').default;
const React = require('react');
const RCTScrollView: $FlowFixMe = requireNativeComponent('RCTScrollView');
function mockScrollView(BaseComponent: $FlowFixMe) {
class ScrollViewMock extends BaseComponent {
render(): React.Element<typeof RCTScrollView> {
return (
<RCTScrollView {...this.props}>
{this.props.refreshControl}
<View>{this.props.children}</View>
</RCTScrollView>
);
}
}
return ScrollViewMock;
}
module.exports = (mockScrollView: $FlowFixMe);

View File

@@ -0,0 +1,16 @@
/**
* 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.
*
* @format
*/
'use strict';
const NodeEnv = require('jest-environment-node').TestEnvironment;
module.exports = class ReactNativeEnv extends NodeEnv {
customExportConditions = ['require', 'react-native'];
};

View File

@@ -0,0 +1,30 @@
/**
* 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
* @oncall react_native
*/
import * as React from 'react';
import ShallowRenderer from 'react-shallow-renderer';
import TestRenderer from 'react-test-renderer';
const renderer = new ShallowRenderer();
export const shallow = (Component: React.Element<any>): any => {
const Wrapper = (): React.Element<any> => Component;
return renderer.render(<Wrapper />);
};
export const shallowRender = (Component: React.Element<any>): any => {
return renderer.render(Component);
};
export const create = (Component: React.Element<any>): any => {
return TestRenderer.create(Component);
};

View File

@@ -0,0 +1,411 @@
/**
* 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.
*
* @format
*/
'use strict';
const MockNativeMethods = jest.requireActual('./MockNativeMethods');
const mockComponent = jest.requireActual('./mockComponent');
jest.requireActual('@react-native/js-polyfills/error-guard');
Object.defineProperties(global, {
__DEV__: {
configurable: true,
enumerable: true,
value: true,
writable: true,
},
cancelAnimationFrame: {
configurable: true,
enumerable: true,
value: id => clearTimeout(id),
writable: true,
},
nativeFabricUIManager: {
configurable: true,
enumerable: true,
value: {},
writable: true,
},
performance: {
configurable: true,
enumerable: true,
value: {
now: jest.fn(Date.now),
},
writable: true,
},
regeneratorRuntime: {
configurable: true,
enumerable: true,
value: jest.requireActual('regenerator-runtime/runtime'),
writable: true,
},
requestAnimationFrame: {
configurable: true,
enumerable: true,
value: callback => setTimeout(() => callback(jest.now()), 0),
writable: true,
},
window: {
configurable: true,
enumerable: true,
value: global,
writable: true,
},
});
jest
.mock('../Libraries/Core/InitializeCore', () => {})
.mock('../Libraries/Core/NativeExceptionsManager')
.mock('../Libraries/ReactNative/UIManager', () => ({
AndroidViewPager: {
Commands: {
setPage: jest.fn(),
setPageWithoutAnimation: jest.fn(),
},
},
blur: jest.fn(),
createView: jest.fn(),
customBubblingEventTypes: {},
customDirectEventTypes: {},
dispatchViewManagerCommand: jest.fn(),
focus: jest.fn(),
getViewManagerConfig: jest.fn(name => {
if (name === 'AndroidDrawerLayout') {
return {
Constants: {
DrawerPosition: {
Left: 10,
},
},
};
}
}),
hasViewManagerConfig: jest.fn(name => {
return name === 'AndroidDrawerLayout';
}),
measure: jest.fn(),
manageChildren: jest.fn(),
setChildren: jest.fn(),
updateView: jest.fn(),
AndroidDrawerLayout: {
Constants: {
DrawerPosition: {
Left: 10,
},
},
},
AndroidTextInput: {
Commands: {},
},
ScrollView: {
Constants: {},
},
View: {
Constants: {},
},
}))
.mock('../Libraries/Image/Image', () => {
const Image = mockComponent('../Libraries/Image/Image');
Image.getSize = jest.fn();
Image.getSizeWithHeaders = jest.fn();
Image.prefetch = jest.fn();
Image.prefetchWithMetadata = jest.fn();
Image.queryCache = jest.fn();
Image.resolveAssetSource = jest.fn();
return Image;
})
.mock('../Libraries/Text/Text', () =>
mockComponent('../Libraries/Text/Text', MockNativeMethods),
)
.mock('../Libraries/Components/TextInput/TextInput', () =>
mockComponent('../Libraries/Components/TextInput/TextInput', {
...MockNativeMethods,
isFocused: jest.fn(),
clear: jest.fn(),
getNativeRef: jest.fn(),
}),
)
.mock('../Libraries/Modal/Modal', () => {
const baseComponent = mockComponent('../Libraries/Modal/Modal');
const mockModal = jest.requireActual('./mockModal');
return mockModal(baseComponent);
})
.mock('../Libraries/Components/View/View', () =>
mockComponent('../Libraries/Components/View/View', MockNativeMethods),
)
.mock('../Libraries/Components/AccessibilityInfo/AccessibilityInfo', () => ({
__esModule: true,
default: {
addEventListener: jest.fn(),
announceForAccessibility: jest.fn(),
isAccessibilityServiceEnabled: jest.fn(),
isBoldTextEnabled: jest.fn(),
isGrayscaleEnabled: jest.fn(),
isInvertColorsEnabled: jest.fn(),
isReduceMotionEnabled: jest.fn(),
prefersCrossFadeTransitions: jest.fn(),
isReduceTransparencyEnabled: jest.fn(),
isScreenReaderEnabled: jest.fn(() => Promise.resolve(false)),
setAccessibilityFocus: jest.fn(),
sendAccessibilityEvent: jest.fn(),
getRecommendedTimeoutMillis: jest.fn(),
},
}))
.mock('../Libraries/Components/Clipboard/Clipboard', () => ({
getString: jest.fn(() => ''),
setString: jest.fn(),
}))
.mock('../Libraries/Components/RefreshControl/RefreshControl', () =>
jest.requireActual(
'../Libraries/Components/RefreshControl/__mocks__/RefreshControlMock',
),
)
.mock('../Libraries/Components/ScrollView/ScrollView', () => {
const baseComponent = mockComponent(
'../Libraries/Components/ScrollView/ScrollView',
{
...MockNativeMethods,
getScrollResponder: jest.fn(),
getScrollableNode: jest.fn(),
getInnerViewNode: jest.fn(),
getInnerViewRef: jest.fn(),
getNativeScrollRef: jest.fn(),
scrollTo: jest.fn(),
scrollToEnd: jest.fn(),
flashScrollIndicators: jest.fn(),
scrollResponderZoomTo: jest.fn(),
scrollResponderScrollNativeHandleToKeyboard: jest.fn(),
},
);
const mockScrollView = jest.requireActual('./mockScrollView');
return mockScrollView(baseComponent);
})
.mock('../Libraries/Components/ActivityIndicator/ActivityIndicator', () => ({
__esModule: true,
default: mockComponent(
'../Libraries/Components/ActivityIndicator/ActivityIndicator',
null,
true,
),
}))
.mock('../Libraries/AppState/AppState', () => ({
addEventListener: jest.fn(() => ({
remove: jest.fn(),
})),
removeEventListener: jest.fn(),
currentState: jest.fn(),
}))
.mock('../Libraries/Linking/Linking', () => ({
openURL: jest.fn(),
canOpenURL: jest.fn(() => Promise.resolve(true)),
openSettings: jest.fn(),
addEventListener: jest.fn(),
getInitialURL: jest.fn(() => Promise.resolve()),
sendIntent: jest.fn(),
}))
// Mock modules defined by the native layer (ex: Objective-C, Java)
.mock('../Libraries/BatchedBridge/NativeModules', () => ({
AlertManager: {
alertWithArgs: jest.fn(),
},
AsyncLocalStorage: {
multiGet: jest.fn((keys, callback) =>
process.nextTick(() => callback(null, [])),
),
multiSet: jest.fn((entries, callback) =>
process.nextTick(() => callback(null)),
),
multiRemove: jest.fn((keys, callback) =>
process.nextTick(() => callback(null)),
),
multiMerge: jest.fn((entries, callback) =>
process.nextTick(() => callback(null)),
),
clear: jest.fn(callback => process.nextTick(() => callback(null))),
getAllKeys: jest.fn(callback =>
process.nextTick(() => callback(null, [])),
),
},
DeviceInfo: {
getConstants() {
return {
Dimensions: {
window: {
fontScale: 2,
height: 1334,
scale: 2,
width: 750,
},
screen: {
fontScale: 2,
height: 1334,
scale: 2,
width: 750,
},
},
};
},
},
DevSettings: {
addMenuItem: jest.fn(),
reload: jest.fn(),
},
ImageLoader: {
getSize: jest.fn(url => Promise.resolve([320, 240])),
prefetchImage: jest.fn(),
},
ImageViewManager: {
getSize: jest.fn((uri, success) =>
process.nextTick(() => success(320, 240)),
),
prefetchImage: jest.fn(),
},
KeyboardObserver: {
addListener: jest.fn(),
removeListeners: jest.fn(),
},
Networking: {
sendRequest: jest.fn(),
abortRequest: jest.fn(),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
PlatformConstants: {
getConstants() {
return {};
},
},
PushNotificationManager: {
presentLocalNotification: jest.fn(),
scheduleLocalNotification: jest.fn(),
cancelAllLocalNotifications: jest.fn(),
removeAllDeliveredNotifications: jest.fn(),
getDeliveredNotifications: jest.fn(callback =>
process.nextTick(() => []),
),
removeDeliveredNotifications: jest.fn(),
setApplicationIconBadgeNumber: jest.fn(),
getApplicationIconBadgeNumber: jest.fn(callback =>
process.nextTick(() => callback(0)),
),
cancelLocalNotifications: jest.fn(),
getScheduledLocalNotifications: jest.fn(callback =>
process.nextTick(() => callback()),
),
requestPermissions: jest.fn(() =>
Promise.resolve({alert: true, badge: true, sound: true}),
),
abandonPermissions: jest.fn(),
checkPermissions: jest.fn(callback =>
process.nextTick(() =>
callback({alert: true, badge: true, sound: true}),
),
),
getInitialNotification: jest.fn(() => Promise.resolve(null)),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
SourceCode: {
getConstants() {
return {
scriptURL: null,
};
},
},
StatusBarManager: {
setColor: jest.fn(),
setStyle: jest.fn(),
setHidden: jest.fn(),
setNetworkActivityIndicatorVisible: jest.fn(),
setBackgroundColor: jest.fn(),
setTranslucent: jest.fn(),
getConstants: () => ({
HEIGHT: 42,
}),
},
Timing: {
createTimer: jest.fn(),
deleteTimer: jest.fn(),
},
UIManager: {},
BlobModule: {
getConstants: () => ({BLOB_URI_SCHEME: 'content', BLOB_URI_HOST: null}),
addNetworkingHandler: jest.fn(),
enableBlobSupport: jest.fn(),
disableBlobSupport: jest.fn(),
createFromParts: jest.fn(),
sendBlob: jest.fn(),
release: jest.fn(),
},
WebSocketModule: {
connect: jest.fn(),
send: jest.fn(),
sendBinary: jest.fn(),
ping: jest.fn(),
close: jest.fn(),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
I18nManager: {
allowRTL: jest.fn(),
forceRTL: jest.fn(),
swapLeftAndRightInRTL: jest.fn(),
getConstants: () => ({
isRTL: false,
doLeftAndRightSwapInRTL: true,
}),
},
}))
.mock('../Libraries/NativeComponent/NativeComponentRegistry', () => {
return {
get: jest.fn((name, viewConfigProvider) => {
return jest.requireActual('./mockNativeComponent').default(name);
}),
getWithFallback_DEPRECATED: jest.fn((name, viewConfigProvider) => {
return jest.requireActual('./mockNativeComponent').default(name);
}),
setRuntimeConfigProvider: jest.fn(),
};
})
.mock('../Libraries/ReactNative/requireNativeComponent', () => {
return jest.requireActual('./mockNativeComponent');
})
.mock(
'../Libraries/Utilities/verifyComponentAttributeEquivalence',
() => function () {},
)
.mock('../Libraries/Vibration/Vibration', () => ({
vibrate: jest.fn(),
cancel: jest.fn(),
}))
.mock('../Libraries/Components/View/ViewNativeComponent', () => {
const React = require('react');
const Component = class extends React.Component {
render() {
return React.createElement('View', this.props, this.props.children);
}
};
Component.displayName = 'View';
return {
__esModule: true,
default: Component,
};
})
// In tests, we can use the default version instead of the one using
// dependency injection.
.mock('../Libraries/ReactNative/RendererProxy', () => {
return jest.requireActual(
'../Libraries/ReactNative/RendererImplementation',
);
});