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,106 @@
import invariant from 'invariant';
import { NativeEventEmitter, Platform } from 'react-native';
const nativeEmitterSubscriptionKey = '@@nativeEmitterSubscription@@';
type NativeModule = {
__expo_module_name__?: string;
startObserving?: () => void;
stopObserving?: () => void;
// Erase these types as they would conflict with the new NativeModule type.
// This EventEmitter is deprecated anyway.
addListener?: any;
removeListeners?: any;
};
// @needsAudit
export type Subscription = {
/**
* A method to unsubscribe the listener.
*/
remove: () => void;
};
export class EventEmitter {
_listenerCount = 0;
// @ts-expect-error
_nativeModule: NativeModule;
// @ts-expect-error
_eventEmitter: NativeEventEmitter;
constructor(nativeModule: NativeModule) {
// If the native module is a new module, just return it back as it's already an event emitter.
// This is for backwards compatibility until we stop using this legacy class in other packages.
if (nativeModule.__expo_module_name__) {
// @ts-expect-error
return nativeModule;
}
this._nativeModule = nativeModule;
this._eventEmitter = new NativeEventEmitter(nativeModule as any);
}
addListener<T>(eventName: string, listener: (event: T) => void): Subscription {
if (!this._listenerCount && Platform.OS !== 'ios' && this._nativeModule.startObserving) {
this._nativeModule.startObserving();
}
this._listenerCount++;
const nativeEmitterSubscription = this._eventEmitter.addListener(eventName, listener);
const subscription = {
[nativeEmitterSubscriptionKey]: nativeEmitterSubscription,
remove: () => {
this.removeSubscription(subscription);
},
};
return subscription;
}
removeAllListeners(eventName: string): void {
// @ts-ignore: the EventEmitter interface has been changed in react-native@0.64.0
const removedListenerCount = this._eventEmitter.listenerCount
? // @ts-ignore: this is available since 0.64
this._eventEmitter.listenerCount(eventName)
: // @ts-ignore: this is available in older versions
this._eventEmitter.listeners(eventName).length;
this._eventEmitter.removeAllListeners(eventName);
this._listenerCount -= removedListenerCount;
invariant(
this._listenerCount >= 0,
`EventEmitter must have a non-negative number of listeners`
);
if (!this._listenerCount && Platform.OS !== 'ios' && this._nativeModule.stopObserving) {
this._nativeModule.stopObserving();
}
}
removeSubscription(subscription: Subscription): void {
const nativeEmitterSubscription = subscription[nativeEmitterSubscriptionKey];
if (!nativeEmitterSubscription) {
return;
}
if ('remove' in nativeEmitterSubscription) {
nativeEmitterSubscription.remove();
}
this._listenerCount--;
// Ensure that the emitter's internal state remains correct even if `removeSubscription` is
// called again with the same subscription
delete subscription[nativeEmitterSubscriptionKey];
// Release closed-over references to the emitter
subscription.remove = () => {};
if (!this._listenerCount && Platform.OS !== 'ios' && this._nativeModule.stopObserving) {
this._nativeModule.stopObserving();
}
}
emit(eventName: string, ...params: any[]): void {
this._eventEmitter.emit(eventName, ...params);
}
}

View File

@@ -0,0 +1,6 @@
import { ensureNativeModulesAreInstalled } from './ensureNativeModulesAreInstalled';
import type { NativeModule } from './ts-declarations/NativeModule';
ensureNativeModulesAreInstalled();
export default globalThis.expo.NativeModule as typeof NativeModule;

View File

@@ -0,0 +1,81 @@
// Copyright © 2024 650 Industries.
// NOTE: Forcing this to be a client boundary so the errors are a bit clearer. In the future we can
// make this a shim on the server by ignoring the globals that are missing in React Server contexts (Node.js).
'use client';
import { NativeModules } from 'react-native';
import { ProxyNativeModule } from './NativeModulesProxy.types';
const LegacyNativeProxy = NativeModules.NativeUnimoduleProxy;
// Fixes `cannot find name 'global'.` in tests
// @ts-ignore
const ExpoNativeProxy = global.expo?.modules?.NativeModulesProxy;
const modulesConstantsKey = 'modulesConstants';
const exportedMethodsKey = 'exportedMethods';
const NativeModulesProxy: { [moduleName: string]: ProxyNativeModule } = {};
if (LegacyNativeProxy) {
// use JSI proxy if available, fallback to legacy RN proxy
const NativeProxy = ExpoNativeProxy ?? LegacyNativeProxy;
Object.keys(NativeProxy[exportedMethodsKey]).forEach((moduleName) => {
// copy constants
NativeModulesProxy[moduleName] = NativeProxy[modulesConstantsKey][moduleName] || {};
// copy methods
NativeProxy[exportedMethodsKey][moduleName].forEach((methodInfo) => {
NativeModulesProxy[moduleName][methodInfo.name] = (...args: unknown[]): Promise<any> => {
// Use the new proxy to call methods on legacy modules, if possible.
if (ExpoNativeProxy?.callMethod) {
return ExpoNativeProxy.callMethod(moduleName, methodInfo.name, args);
}
// Otherwise fall back to the legacy proxy.
// This is deprecated and might be removed in SDK47 or later.
const { key, argumentsCount } = methodInfo;
if (argumentsCount !== args.length) {
return Promise.reject(
new Error(
`Native method ${moduleName}.${methodInfo.name} expects ${argumentsCount} ${
argumentsCount === 1 ? 'argument' : 'arguments'
} but received ${args.length}`
)
);
}
return LegacyNativeProxy.callMethod(moduleName, key, args);
};
});
// These are called by EventEmitter (which is a wrapper for NativeEventEmitter)
// only on iOS and they use iOS-specific native module, EXReactNativeEventEmitter.
//
// On Android only {start,stop}Observing are called on the native module
// and these should be exported as Expo methods.
//
// Before the RN 65, addListener/removeListeners weren't called on Android. However, it no longer stays true.
// See https://github.com/facebook/react-native/commit/f5502fbda9fe271ff6e1d0da773a3a8ee206a453.
// That's why, we check if the `EXReactNativeEventEmitter` exists and only if yes, we use it in the listener implementation.
// Otherwise, those methods are NOOP.
if (NativeModules.EXReactNativeEventEmitter) {
NativeModulesProxy[moduleName].addListener = (...args) =>
NativeModules.EXReactNativeEventEmitter.addProxiedListener(moduleName, ...args);
NativeModulesProxy[moduleName].removeListeners = (...args) =>
NativeModules.EXReactNativeEventEmitter.removeProxiedListeners(moduleName, ...args);
} else {
// Fixes on Android:
// WARN `new NativeEventEmitter()` was called with a non-null argument without the required `addListener` method.
// WARN `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method.
NativeModulesProxy[moduleName].addListener = () => {};
NativeModulesProxy[moduleName].removeListeners = () => {};
}
});
} else {
console.warn(
`The "EXNativeModulesProxy" native module is not exported through NativeModules; verify that expo-modules-core's native code is linked properly`
);
}
export default NativeModulesProxy;

View File

@@ -0,0 +1,9 @@
import { ProxyNativeModule } from './NativeModulesProxy.types';
// We default to an empty object shim wherever we don't have an environment-specific implementation
/**
* @deprecated `NativeModulesProxy` is deprecated and might be removed in the future releases.
* Use `requireNativeModule` or `requireOptionalNativeModule` instead.
*/
export default {} as { [moduleName: string]: ProxyNativeModule };

View File

@@ -0,0 +1,5 @@
export type ProxyNativeModule = {
[propertyName: string]: any;
addListener?: (eventName: string) => void;
removeListeners?: (count: number) => void;
};

View File

@@ -0,0 +1,112 @@
// Copyright © 2024 650 Industries.
'use client';
import React from 'react';
import { findNodeHandle, NativeModules, HostComponent } from 'react-native';
import * as NativeComponentRegistry from 'react-native/Libraries/NativeComponent/NativeComponentRegistry';
import { requireNativeModule } from './requireNativeModule';
// To make the transition from React Native's `requireNativeComponent` to Expo's
// `requireNativeViewManager` as easy as possible, `requireNativeViewManager` is a drop-in
// replacement for `requireNativeComponent`.
//
// For each view manager, we create a wrapper component that accepts all of the props available to
// the author of the universal module. This wrapper component splits the props into two sets: props
// passed to React Native's View (ex: style, testID) and custom view props, which are passed to the
// adapter view component in a prop called `proxiedProperties`.
/**
* A map that caches registered native components.
*/
const nativeComponentsCache = new Map<string, HostComponent<any>>();
/**
* Requires a React Native component using the static view config from an Expo module.
*/
function requireNativeComponent<Props>(viewName: string): HostComponent<Props> {
return NativeComponentRegistry.get<Props>(viewName, () => {
const viewModuleName = viewName.replace('ViewManagerAdapter_', '');
const expoViewConfig = globalThis.expo?.getViewConfig(viewModuleName);
if (!expoViewConfig) {
console.warn('Unable to get the view config for %s', viewModuleName);
}
return {
uiViewClassName: viewName,
...expoViewConfig,
};
});
}
/**
* Requires a React Native component from cache if possible. This prevents
* "Tried to register two views with the same name" errors on fast refresh, but
* also when there are multiple versions of the same package with native component.
*/
function requireCachedNativeComponent<Props>(viewName: string): HostComponent<Props> {
const cachedNativeComponent = nativeComponentsCache.get(viewName);
if (!cachedNativeComponent) {
const nativeComponent = requireNativeComponent<Props>(viewName);
nativeComponentsCache.set(viewName, nativeComponent);
return nativeComponent;
}
return cachedNativeComponent;
}
/**
* A drop-in replacement for `requireNativeComponent`.
*/
export function requireNativeViewManager<P>(viewName: string): React.ComponentType<P> {
const { viewManagersMetadata } = NativeModules.NativeUnimoduleProxy;
const viewManagerConfig = viewManagersMetadata?.[viewName];
if (__DEV__ && !viewManagerConfig) {
const exportedViewManagerNames = Object.keys(viewManagersMetadata).join(', ');
console.warn(
`The native view manager required by name (${viewName}) from NativeViewManagerAdapter isn't exported by expo-modules-core. Views of this type may not render correctly. Exported view managers: [${exportedViewManagerNames}].`
);
}
// Set up the React Native native component, which is an adapter to the universal module's view
// manager
const reactNativeViewName = `ViewManagerAdapter_${viewName}`;
const ReactNativeComponent = requireCachedNativeComponent(reactNativeViewName);
class NativeComponent extends React.PureComponent<P> {
static displayName = viewName;
// This will be accessed from native when the prototype functions are called,
// in order to find the associated native view.
nativeTag: number | null = null;
componentDidMount(): void {
this.nativeTag = findNodeHandle(this);
}
render(): React.ReactNode {
return <ReactNativeComponent {...this.props} />;
}
}
try {
const nativeModule = requireNativeModule(viewName);
const nativeViewPrototype = nativeModule.ViewPrototype;
if (nativeViewPrototype) {
// Assign native view functions to the component prototype so they can be accessed from the ref.
Object.assign(NativeComponent.prototype, nativeViewPrototype);
}
} catch {
// `requireNativeModule` may throw an error when the native module cannot be found.
// In some tests we don't mock the entire modules, but we do want to mock native views. For now,
// until we still have to support the legacy modules proxy and don't have better ways to mock,
// let's just gracefully skip assigning the prototype functions.
// See: https://github.com/expo/expo/blob/main/packages/expo-modules-core/src/__tests__/NativeViewManagerAdapter-test.native.tsx
}
return NativeComponent;
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { UnavailabilityError } from './errors/UnavailabilityError';
/**
* A drop-in replacement for `requireNativeComponent`.
*/
export function requireNativeViewManager<P = any>(viewName: string): React.ComponentType<P> {
throw new UnavailabilityError('expo-modules-core', 'requireNativeViewManager');
}

View File

@@ -0,0 +1,86 @@
// Copyright © 2024 650 Industries.
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import { PermissionResponse } from './PermissionsInterface';
// These types are identical, but improves the readability for suggestions in editors
type RequestPermissionMethod<Permission extends PermissionResponse> = () => Promise<Permission>;
type GetPermissionMethod<Permission extends PermissionResponse> = () => Promise<Permission>;
interface PermissionHookMethods<Permission extends PermissionResponse, Options = never> {
/** The permission method that requests the user to grant permission. */
requestMethod: (options?: Options) => Promise<Permission>;
/** The permission method that only fetches the current permission status. */
getMethod: (options?: Options) => Promise<Permission>;
}
interface PermissionHookBehavior {
/** If the hook should automatically fetch the current permission status, without asking the user. */
get?: boolean;
/** If the hook should automatically request the user to grant permission. */
request?: boolean;
}
export type PermissionHookOptions<Options extends object> = PermissionHookBehavior & Options;
/**
* Get or request permission for protected functionality within the app.
* It uses separate permission requesters to interact with a single permission.
* By default, the hook will only retrieve the permission status.
*/
function usePermission<Permission extends PermissionResponse, Options extends object>(
methods: PermissionHookMethods<Permission, Options>,
options?: PermissionHookOptions<Options>
): [Permission | null, RequestPermissionMethod<Permission>, GetPermissionMethod<Permission>] {
const isMounted = useRef(true);
const [status, setStatus] = useState<Permission | null>(null);
const { get = true, request = false, ...permissionOptions } = options || {};
const getPermission = useCallback(async () => {
const response = await methods.getMethod(
Object.keys(permissionOptions).length > 0 ? (permissionOptions as Options) : undefined
);
if (isMounted.current) setStatus(response);
return response;
}, [methods.getMethod]);
const requestPermission = useCallback(async () => {
const response = await methods.requestMethod(
Object.keys(permissionOptions).length > 0 ? (permissionOptions as Options) : undefined
);
if (isMounted.current) setStatus(response);
return response;
}, [methods.requestMethod]);
useEffect(
function runMethods() {
if (request) requestPermission();
if (!request && get) getPermission();
},
[get, request, requestPermission, getPermission]
);
// Workaround for unmounting components receiving state updates
useEffect(function didMount() {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return [status, requestPermission, getPermission];
}
/**
* Create a new permission hook with the permission methods built-in.
* This can be used to quickly create specific permission hooks in every module.
*/
export function createPermissionHook<Permission extends PermissionResponse, Options extends object>(
methods: PermissionHookMethods<Permission, Options>
) {
return (options?: PermissionHookOptions<Options>) =>
usePermission<Permission, Options>(methods, options);
}

View File

@@ -0,0 +1,43 @@
export enum PermissionStatus {
/**
* User has granted the permission.
*/
GRANTED = 'granted',
/**
* User hasn't granted or denied the permission yet.
*/
UNDETERMINED = 'undetermined',
/**
* User has denied the permission.
*/
DENIED = 'denied',
}
/**
* Permission expiration time. Currently, all permissions are granted permanently.
*/
export type PermissionExpiration = 'never' | number;
/**
* An object obtained by permissions get and request functions.
*/
export interface PermissionResponse {
/**
* Determines the status of the permission.
*/
status: PermissionStatus;
/**
* Determines time when the permission expires.
*/
expires: PermissionExpiration;
/**
* A convenience boolean that indicates if the permission is granted.
*/
granted: boolean;
/**
* Indicates if user can be asked again for specific permission.
* If not, one should be directed to the Settings app
* in order to enable/disable the permission.
*/
canAskAgain: boolean;
}

View File

@@ -0,0 +1,53 @@
import { Platform as ReactNativePlatform, PlatformOSType } from 'react-native';
import {
isDOMAvailable,
canUseEventListeners,
canUseViewport,
isAsyncDebugging,
} from './environment/browser';
export type PlatformSelectOSType = PlatformOSType | 'native' | 'electron' | 'default';
export type PlatformSelect = <T>(specifics: { [platform in PlatformSelectOSType]?: T }) => T;
const Platform = {
/**
* Denotes the currently running platform.
* Can be one of ios, android, web.
*/
OS: ReactNativePlatform.OS,
/**
* Returns the value with the matching platform.
* Object keys can be any of ios, android, native, web, default.
*
* @ios ios, native, default
* @android android, native, default
* @web web, default
*/
select: ReactNativePlatform.select as PlatformSelect,
/**
* Denotes if the DOM API is available in the current environment.
* The DOM is not available in native React runtimes and Node.js.
*/
isDOMAvailable,
/**
* Denotes if the current environment can attach event listeners
* to the window. This will return false in native React
* runtimes and Node.js.
*/
canUseEventListeners,
/**
* Denotes if the current environment can inspect properties of the
* screen on which the current window is being rendered. This will
* return false in native React runtimes and Node.js.
*/
canUseViewport,
/**
* If the JavaScript is being executed in a remote JavaScript environment.
* When `true`, synchronous native invocations cannot be executed.
*/
isAsyncDebugging,
};
export default Platform;

View File

@@ -0,0 +1,10 @@
import React from 'react';
/**
* Create a React ref object that is friendly for snapshots.
* It will be represented as `[React.ref]` in snapshots.
* @returns a React ref object.
*/
export function createSnapshotFriendlyRef<T>(): React.RefObject<T> {
return React.createRef<T>();
}

View File

@@ -0,0 +1,6 @@
import { ensureNativeModulesAreInstalled } from './ensureNativeModulesAreInstalled';
import type { SharedObject } from './ts-declarations/SharedObject';
ensureNativeModulesAreInstalled();
export default globalThis.expo.SharedObject as typeof SharedObject;

View File

@@ -0,0 +1,11 @@
/** A union type for all integer based [`TypedArray` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#typedarray_objects). */
export type IntBasedTypedArray = Int8Array | Int16Array | Int32Array;
/** A union type for all unsigned integer based [`TypedArray` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#typedarray_objects). */
export type UintBasedTypedArray = Uint8Array | Uint8ClampedArray | Uint16Array | Uint32Array;
/** A union type for all floating point based [`TypedArray` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#typedarray_objects). */
export type FloatBasedTypedArray = Float32Array | Float64Array;
/** A [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) describes an array-like view of an underlying binary data buffer. */
export type TypedArray = IntBasedTypedArray | UintBasedTypedArray | FloatBasedTypedArray;

View File

@@ -0,0 +1,5 @@
// It is a no-op function that returns the module implementation without importing CoreModule.
// Actual implementation is located in `createWebModule.web.ts`.
export function createWebModule<ModuleType = any>(moduleImplementation: ModuleType): ModuleType {
return moduleImplementation;
}

View File

@@ -0,0 +1,6 @@
import { NativeModule } from './web/CoreModule';
export function createWebModule<ModuleType = any>(moduleImplementation: ModuleType): ModuleType {
const module = new NativeModule();
return Object.assign(module, moduleImplementation);
}

View File

@@ -0,0 +1,24 @@
import { NativeModules, Platform } from 'react-native';
/**
* Ensures that the native modules are installed in the current runtime.
* Otherwise, it synchronously calls a native function that installs them.
*/
export function ensureNativeModulesAreInstalled(): void {
if (globalThis.expo) {
return;
}
try {
if (Platform.OS === 'web') {
// Requiring web folder sets up the `globalThis.expo` object.
require('./web');
} else {
// TODO: ExpoModulesCore shouldn't be optional here,
// but to keep backwards compatibility let's just ignore it in SDK 50.
// In most cases the modules were already installed from the native side.
NativeModules.ExpoModulesCore?.installModules();
}
} catch (error) {
console.error(`Unable to install Expo modules: ${error}`);
}
}

View File

@@ -0,0 +1,15 @@
declare const global: any;
// In standard node environments there is no DOM API
export const isDOMAvailable = false;
export const canUseEventListeners = false;
export const canUseViewport = false;
export let isAsyncDebugging: boolean = false;
if (__DEV__) {
// These native globals are injected by native React runtimes and not standard browsers
// we can use them to determine if the JS is being executed in Chrome.
isAsyncDebugging =
!global.nativeExtensions && !global.nativeCallSyncHook && !global.RN$Bridgeless;
}

View File

@@ -0,0 +1,14 @@
declare global {
// Add IE-specific interface to Window
interface Window {
attachEvent(event: string, listener: EventListener): boolean;
}
}
// Used for delegating node actions when browser APIs aren't available
// like in SSR websites.
export const isDOMAvailable = typeof window !== 'undefined' && !!window.document?.createElement;
export const canUseEventListeners =
isDOMAvailable && !!(window.addEventListener || window.attachEvent);
export const canUseViewport = isDOMAvailable && !!window.screen;
export const isAsyncDebugging = false;

View File

@@ -0,0 +1,14 @@
/**
* A general error class that should be used for all errors in Expo modules.
* Guarantees a `code` field that can be used to differentiate between different
* types of errors without further subclassing Error.
*/
export class CodedError extends Error {
code: string;
info?: any;
constructor(code: string, message: string) {
super(message);
this.code = code;
}
}

View File

@@ -0,0 +1,16 @@
import { CodedError } from './CodedError';
import Platform from '../Platform';
/**
* A class for errors to be thrown when a property is accessed which is
* unavailable, unsupported, or not currently implemented on the running
* platform.
*/
export class UnavailabilityError extends CodedError {
constructor(moduleName: string, propertyName: string) {
super(
'ERR_UNAVAILABLE',
`The method or property ${moduleName}.${propertyName} is not available on ${Platform.OS}, are you sure you've linked all the native dependencies properly?`
);
}
}

View File

@@ -0,0 +1,51 @@
import { DependencyList, useRef, useMemo, useEffect } from 'react';
import type { SharedObject } from '../ts-declarations/SharedObject';
/**
* Returns a shared object, which is automatically cleaned up when the component is unmounted.
*/
export function useReleasingSharedObject<TSharedObject extends SharedObject>(
factory: () => TSharedObject,
dependencies: DependencyList
): TSharedObject {
const objectRef = useRef<TSharedObject | null>(null);
const isFastRefresh = useRef(false);
const previousDependencies = useRef<DependencyList>(dependencies);
if (objectRef.current == null) {
objectRef.current = factory();
}
const object = useMemo(() => {
let newObject = objectRef.current;
const dependenciesAreEqual =
previousDependencies.current?.length === dependencies.length &&
dependencies.every((value, index) => value === previousDependencies.current[index]);
// If the dependencies have changed, release the previous object and create a new one, otherwise this has been called
// because of a fast refresh, and we don't want to release the object.
if (!newObject || !dependenciesAreEqual) {
objectRef.current?.release();
newObject = factory();
objectRef.current = newObject;
previousDependencies.current = dependencies;
} else {
isFastRefresh.current = true;
}
return newObject;
}, dependencies);
useEffect(() => {
isFastRefresh.current = false;
return () => {
// This will be called on every fast refresh and on unmount, but we only want to release the object on unmount.
if (!isFastRefresh.current && objectRef.current) {
objectRef.current.release();
}
};
}, []);
return object;
}

View File

@@ -0,0 +1,51 @@
import { DeviceEventEmitter } from 'react-native';
import { EventEmitter, Subscription } from './EventEmitter';
import NativeModule from './NativeModule';
import NativeModulesProxy from './NativeModulesProxy';
import { ProxyNativeModule } from './NativeModulesProxy.types';
import { requireNativeViewManager } from './NativeViewManagerAdapter';
import Platform from './Platform';
import SharedObject from './SharedObject';
import { CodedError } from './errors/CodedError';
import { UnavailabilityError } from './errors/UnavailabilityError';
import './sweet/setUpErrorManager.fx';
import './web/index';
export type * from './ts-declarations/global';
export { default as uuid } from './uuid';
export {
DeviceEventEmitter,
EventEmitter,
NativeModulesProxy,
ProxyNativeModule,
Platform,
Subscription,
requireNativeViewManager,
// Globals
SharedObject,
NativeModule,
// Errors
CodedError,
UnavailabilityError,
};
export * from './requireNativeModule';
export * from './createWebModule';
export * from './TypedArrays.types';
/**
* @deprecated renamed to `DeviceEventEmitter`
*/
export const SyntheticPlatformEmitter = DeviceEventEmitter;
export * from './PermissionsInterface';
export * from './PermissionsHook';
export * from './Refs';
export * from './hooks/useReleasingSharedObject';
export * from './reload';

View File

@@ -0,0 +1,13 @@
/**
* Reloads the app.
*
* This function works for both release and debug builds.
*
* Unlike [`Updates.reloadAsync()`](https://docs.expo.dev/versions/latest/sdk/updates/#updatesreloadasync),
* this function does not use a new update even if one is available. It only reloads the app using the same JavaScript bundle that is currently running.
*
* @param reason The reason for reloading the app. This is used only for some platforms.
*/
export async function reloadAppAsync(reason: string = 'Reloaded from JS call'): Promise<void> {
await globalThis.expo?.reloadAppAsync(reason);
}

View File

@@ -0,0 +1,35 @@
import NativeModulesProxy from './NativeModulesProxy';
import { ensureNativeModulesAreInstalled } from './ensureNativeModulesAreInstalled';
/**
* Imports the native module registered with given name. In the first place it tries to load
* the module installed through the JSI host object and then falls back to the bridge proxy module.
* Notice that the modules loaded from the proxy may not support some features like synchronous functions.
*
* @param moduleName Name of the requested native module.
* @returns Object representing the native module.
* @throws Error when there is no native module with given name.
*/
export function requireNativeModule<ModuleType = any>(moduleName: string): ModuleType {
const nativeModule = requireOptionalNativeModule<ModuleType>(moduleName);
if (!nativeModule) {
throw new Error(`Cannot find native module '${moduleName}'`);
}
return nativeModule;
}
/**
* Imports the native module registered with the given name. The same as `requireNativeModule`,
* but returns `null` when the module cannot be found instead of throwing an error.
*
* @param moduleName Name of the requested native module.
* @returns Object representing the native module or `null` when it cannot be found.
*/
export function requireOptionalNativeModule<ModuleType = any>(
moduleName: string
): ModuleType | null {
ensureNativeModulesAreInstalled();
return globalThis.expo?.modules?.[moduleName] ?? NativeModulesProxy[moduleName] ?? null;
}

View File

@@ -0,0 +1,7 @@
export function requireNativeModule(moduleName: string) {
throw new Error(`Cannot find native module '${moduleName}'`);
}
export function requireOptionalNativeModule() {
return null;
}

View File

@@ -0,0 +1,2 @@
import NativeModulesProxy from '../NativeModulesProxy';
export default NativeModulesProxy.ExpoModulesCoreErrorManager;

View File

@@ -0,0 +1,21 @@
import NativeErrorManager from './NativeErrorManager';
import { EventEmitter } from '../EventEmitter';
import Platform from '../Platform';
import { CodedError } from '../errors/CodedError';
if (__DEV__ && Platform.OS === 'android' && NativeErrorManager) {
const onNewException = 'ExpoModulesCoreErrorManager.onNewException';
const onNewWarning = 'ExpoModulesCoreErrorManager.onNewWarning';
const eventEmitter = new EventEmitter(NativeErrorManager);
eventEmitter.addListener(onNewException, ({ message }: { message: string }) => {
console.error(message);
});
eventEmitter.addListener(onNewWarning, ({ message }: { message: string }) => {
console.warn(message);
});
}
// We have to export `CodedError` via global object to use in later in the C++ code.
globalThis.ExpoModulesCore_CodedError = CodedError;

View File

@@ -0,0 +1,65 @@
/**
* Base type of the events map, whose keys represent supported event names
* and values are the signatures of the listener for that specific event.
*/
export type EventsMap = Record<string, (...args: any[]) => void>;
/**
* A subscription object that allows to conveniently remove an event listener from the emitter.
*/
export type EventSubscription = {
/**
* Removes an event listener for which the subscription has been created.
* After calling this function, the listener will no longer receive any events from the emitter.
*/
remove(): void;
};
/**
* A class that provides a consistent API for emitting and listening to events.
* It shares many concepts with other emitter APIs, such as Node's EventEmitter and `fbemitter`.
* When the event is emitted, all of the functions attached to that specific event are called *synchronously*.
* Any values returned by the called listeners are *ignored* and discarded.
* Its implementation is written in C++ and common for all the platforms.
*/
export declare class EventEmitter<TEventsMap extends EventsMap = Record<never, never>> {
/**
* Creates a new event emitter instance.
*/
constructor();
/**
* Adds a listener for the given event name.
*/
addListener<EventName extends keyof TEventsMap>(
eventName: EventName,
listener: TEventsMap[EventName]
): EventSubscription;
/**
* Removes a listener for the given event name.
*/
removeListener<EventName extends keyof TEventsMap>(
eventName: EventName,
listener: TEventsMap[EventName]
): void;
/**
* Removes all listeners for the given event name.
*/
removeAllListeners(eventName: keyof TEventsMap): void;
/**
* Synchronously calls all of the listeners attached to that specific event.
* The event can include any number of arguments that will be passed to the listeners.
*/
emit<EventName extends keyof TEventsMap>(
eventName: EventName,
...args: Parameters<TEventsMap[EventName]>
): void;
/**
* Returns a number of listeners added to the given event.
*/
listenerCount<EventName extends keyof TEventsMap>(eventName: EventName): number;
}

View File

@@ -0,0 +1,5 @@
import { CodedError } from '../errors/CodedError';
declare let global: {
ExpoModulesCore_CodedError: typeof CodedError;
};

View File

@@ -0,0 +1,16 @@
declare module 'react-native/Libraries/EventEmitter/NativeEventEmitter' {
import { EventEmitter } from 'react-native';
interface NativeEventEmitter extends EventEmitter {
new (nativeModule: NativeModule): NativeEventEmitters;
}
const NativeEventEmitter: NativeEventEmitter;
export default NativeEventEmitter;
type NativeModule = {
addListener: (eventType: string) => void;
removeListeners: (count: number) => void;
};
}

View File

@@ -0,0 +1,18 @@
import type { EventEmitter, EventsMap } from './EventEmitter';
/**
* A class for all native modules. Extends the {@link EventEmitter} class.
*/
export declare class NativeModule<
TEventsMap extends EventsMap = Record<never, never>,
> extends EventEmitter<TEventsMap> {
/**
* A prototype of the native component exported by the module.
* @deprecated It will be removed in favor of another API that supports multiple components per module.
* @private
*/
ViewPrototype?: object;
// Ideally if we don't have it, but not all modules have concrete types in `requireNativeModule`.
[key: string]: any;
}

View File

@@ -0,0 +1,16 @@
import type { EventEmitter, EventsMap } from './EventEmitter';
/**
* Base class for all shared objects that extends the EventEmitter class.
* The implementation is written in C++, installed through JSI and common for mobile platforms.
*/
export declare class SharedObject<
TEventsMap extends EventsMap = Record<never, never>,
> extends EventEmitter<TEventsMap> {
/**
* A function that detaches the JS and native objects to let the native object deallocate
* before the JS object gets deallocated by the JS garbagge collector. Any subsequent calls to native
* functions of the object will throw an error as it is no longer associated with its native counterpart.
*/
release(): void;
}

View File

@@ -0,0 +1,65 @@
import type { EventEmitter } from './EventEmitter';
import type { NativeModule } from './NativeModule';
import type { SharedObject } from './SharedObject';
export interface ExpoGlobal {
/**
* Host object that is used to access native Expo modules.
*/
modules: Record<string, any>;
// Natively defined JS classes
/**
* @see EventEmitter
*/
EventEmitter: typeof EventEmitter;
/**
* @see SharedObject
*/
SharedObject: typeof SharedObject;
/**
* @see NativeModule
*/
NativeModule: typeof NativeModule;
// Utils
/**
* Generates a random UUID v4 string.
*/
uuidv4(): string;
/**
* Generates a UUID v5 string representation of the value in the specified namespace.
*/
uuidv5(name: string, namespace: string): string;
/**
* Returns a static view config of the native view with the given name
* or `null` if the view has not been registered.
*/
getViewConfig(viewName: string): ViewConfig | null;
/**
* Reloads the app.
*/
reloadAppAsync(reason: string): Promise<void>;
}
type ViewConfig = {
validAttributes: Record<string, any>;
directEventTypes: Record<string, { registrationName: string }>;
};
/* eslint-disable no-var */
declare global {
/**
* Global object containing all the native bindings installed by Expo.
* This object is not available in projects without the `expo` package installed.
*/
var expo: ExpoGlobal;
}

View File

@@ -0,0 +1 @@
export { default } from './uuid';

View File

@@ -0,0 +1,38 @@
/**
* Convert array of 16 byte values to UUID string format of the form:
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
*/
const byteToHex: string[] = [];
for (let i = 0; i < 256; ++i) {
byteToHex[i] = (i + 0x100).toString(16).substr(1);
}
function bytesToUuid(buf: number[], offset?: number) {
let i = offset || 0;
const bth = byteToHex;
// join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4
return [
bth[buf[i++]],
bth[buf[i++]],
bth[buf[i++]],
bth[buf[i++]],
'-',
bth[buf[i++]],
bth[buf[i++]],
'-',
bth[buf[i++]],
bth[buf[i++]],
'-',
bth[buf[i++]],
bth[buf[i++]],
'-',
bth[buf[i++]],
bth[buf[i++]],
bth[buf[i++]],
bth[buf[i++]],
bth[buf[i++]],
bth[buf[i++]],
].join('');
}
export default bytesToUuid;

View File

@@ -0,0 +1,110 @@
// Adapted from Chris Veness' SHA1 code at
// http://www.movable-type.co.uk/scripts/sha1.html
'use strict';
function f(s: number, x: number, y: number, z: number) {
switch (s) {
case 0:
return (x & y) ^ (~x & z);
case 1:
return x ^ y ^ z;
case 2:
return (x & y) ^ (x & z) ^ (y & z);
case 3:
return x ^ y ^ z;
default:
return 0;
}
}
function ROTL(x: number, n: number) {
return (x << n) | (x >>> (32 - n));
}
function sha1(bytes: number[] | string) {
const K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
const H = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
if (typeof bytes == 'string') {
const msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
bytes = new Array(msg.length);
for (let i = 0; i < msg.length; i++) bytes[i] = msg.charCodeAt(i);
}
bytes.push(0x80);
const l = bytes.length / 4 + 2;
const N = Math.ceil(l / 16);
const M = new Array(N);
for (let i = 0; i < N; i++) {
M[i] = new Array(16);
for (let j = 0; j < 16; j++) {
M[i][j] =
(bytes[i * 64 + j * 4] << 24) |
(bytes[i * 64 + j * 4 + 1] << 16) |
(bytes[i * 64 + j * 4 + 2] << 8) |
bytes[i * 64 + j * 4 + 3];
}
}
M[N - 1][14] = ((bytes.length - 1) * 8) / Math.pow(2, 32);
M[N - 1][14] = Math.floor(M[N - 1][14]);
M[N - 1][15] = ((bytes.length - 1) * 8) & 0xffffffff;
for (let i = 0; i < N; i++) {
const W = new Array(80);
for (let t = 0; t < 16; t++) W[t] = M[i][t];
for (let t = 16; t < 80; t++) {
W[t] = ROTL(W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16], 1);
}
let a = H[0];
let b = H[1];
let c = H[2];
let d = H[3];
let e = H[4];
for (let t = 0; t < 80; t++) {
const s = Math.floor(t / 20);
const T = (ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[t]) >>> 0;
e = d;
d = c;
c = ROTL(b, 30) >>> 0;
b = a;
a = T;
}
H[0] = (H[0] + a) >>> 0;
H[1] = (H[1] + b) >>> 0;
H[2] = (H[2] + c) >>> 0;
H[3] = (H[3] + d) >>> 0;
H[4] = (H[4] + e) >>> 0;
}
return [
(H[0] >> 24) & 0xff,
(H[0] >> 16) & 0xff,
(H[0] >> 8) & 0xff,
H[0] & 0xff,
(H[1] >> 24) & 0xff,
(H[1] >> 16) & 0xff,
(H[1] >> 8) & 0xff,
H[1] & 0xff,
(H[2] >> 24) & 0xff,
(H[2] >> 16) & 0xff,
(H[2] >> 8) & 0xff,
H[2] & 0xff,
(H[3] >> 24) & 0xff,
(H[3] >> 16) & 0xff,
(H[3] >> 8) & 0xff,
H[3] & 0xff,
(H[4] >> 24) & 0xff,
(H[4] >> 16) & 0xff,
(H[4] >> 8) & 0xff,
H[4] & 0xff,
];
}
export default sha1;

View File

@@ -0,0 +1,67 @@
import bytesToUuid from './bytesToUuid';
function uuidToBytes(uuid: string) {
// Note: We assume we're being passed a valid uuid string
const bytes: number[] = [];
uuid.replace(/[a-fA-F0-9]{2}/g, (hex: string) => {
bytes.push(parseInt(hex, 16));
return '';
});
return bytes;
}
function stringToBytes(str: string) {
str = unescape(encodeURIComponent(str)); // UTF8 escape
const bytes: number[] = new Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
export default function (
name: string,
version: number,
hashfunc: (bytes: number[] | string) => number[]
) {
const generateUUID = function (
value: number[] | string,
namespace: number[] | string,
buf?: number[],
offset?: number
): string {
const off = (buf && offset) || 0;
if (typeof value == 'string') value = stringToBytes(value);
if (typeof namespace == 'string') namespace = uuidToBytes(namespace);
if (!Array.isArray(value)) throw TypeError('value must be an array of bytes');
if (!Array.isArray(namespace) || namespace.length !== 16)
throw TypeError('namespace must be uuid string or an Array of 16 byte values');
// Per 4.3
const bytes = hashfunc(namespace.concat(value));
bytes[6] = (bytes[6] & 0x0f) | version;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
if (buf) {
for (let idx = 0; idx < 16; ++idx) {
buf[off + idx] = bytes[idx];
}
}
return bytesToUuid(bytes);
};
// Function#name is not settable on some platforms (#270)
try {
generateUUID.name = name;
} catch {}
// Pre-defined namespaces, per Appendix C
generateUUID.DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
generateUUID.URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
return generateUUID;
}

View File

@@ -0,0 +1,38 @@
import bytesToUuid from './lib/bytesToUuid';
import { UUID, Uuidv5Namespace } from './uuid.types';
const nativeUuidv4 = globalThis?.expo?.uuidv4;
const nativeUuidv5 = globalThis?.expo?.uuidv5;
function uuidv4(): string {
if (!nativeUuidv4) {
throw Error(
"Native UUID version 4 generator implementation wasn't found in `expo-modules-core`"
);
}
return nativeUuidv4();
}
function uuidv5(name: string, namespace: string | number[]) {
const parsedNamespace =
Array.isArray(namespace) && namespace.length === 16 ? bytesToUuid(namespace) : namespace;
// If parsed namespace is still an array it means that it wasn't valid
if (Array.isArray(parsedNamespace)) {
throw new Error('`namespace` must be a valid UUID string or an Array of 16 byte values');
}
if (!nativeUuidv5) {
throw Error("Native UUID type 5 generator implementation wasn't found in `expo-modules-core`");
}
return nativeUuidv5(name, parsedNamespace);
}
const uuid: UUID = {
v4: uuidv4,
v5: uuidv5,
namespace: Uuidv5Namespace,
};
export default uuid;

View File

@@ -0,0 +1,25 @@
/**
* Collection of utilities used for generating Universally Unique Identifiers.
*/
export type UUID = {
/**
* A UUID generated randomly.
*/
v4: () => string;
/**
* A UUID generated based on the `value` and `namespace` parameters, which always produces the same result for the same inputs.
*/
v5: (name: string, namespace: string | number[]) => string;
namespace: typeof Uuidv5Namespace;
};
/**
* Default namespaces for UUID v5 defined in RFC 4122
*/
export enum Uuidv5Namespace {
// Source of the UUIDs: https://datatracker.ietf.org/doc/html/rfc4122
dns = '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
url = '6ba7b811-9dad-11d1-80b4-00c04fd430c8',
oid = '6ba7b812-9dad-11d1-80b4-00c04fd430c8',
x500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8',
}

View File

@@ -0,0 +1,24 @@
import sha1 from './lib/sha1';
import v35 from './lib/v35';
import { UUID, Uuidv5Namespace } from './uuid.types';
function uuidv4(): string {
// Crypto needs to be required when run in Node.js environment.
const cryptoObject =
typeof crypto === 'undefined' || typeof crypto.randomUUID === 'undefined'
? require('crypto')
: crypto;
if (!cryptoObject?.randomUUID) {
throw Error("The browser doesn't support `crypto.randomUUID` function");
}
return cryptoObject.randomUUID();
}
const uuid: UUID = {
v4: uuidv4,
v5: v35('v5', 0x50, sha1),
namespace: Uuidv5Namespace,
};
export default uuid;

View File

@@ -0,0 +1,86 @@
import type {
EventEmitter as EventEmitterType,
EventSubscription,
EventsMap,
} from '../ts-declarations/EventEmitter';
import type { NativeModule as NativeModuleType } from '../ts-declarations/NativeModule';
import type { SharedObject as SharedObjectType } from '../ts-declarations/SharedObject';
import uuid from '../uuid';
class EventEmitter<TEventsMap extends EventsMap> implements EventEmitterType {
private listeners?: Map<keyof TEventsMap, Set<Function>>;
addListener<EventName extends keyof TEventsMap>(
eventName: EventName,
listener: TEventsMap[EventName]
): EventSubscription {
if (!this.listeners) {
this.listeners = new Map();
}
if (!this.listeners?.has(eventName)) {
this.listeners?.set(eventName, new Set());
}
this.listeners?.get(eventName)?.add(listener);
return {
remove: () => {
this.removeListener(eventName, listener);
},
};
}
removeListener<EventName extends keyof TEventsMap>(
eventName: EventName,
listener: TEventsMap[EventName]
): void {
this.listeners?.get(eventName)?.delete(listener);
}
removeAllListeners<EventName extends keyof TEventsMap>(eventName: EventName): void {
this.listeners?.get(eventName)?.clear();
}
emit<EventName extends keyof TEventsMap>(
eventName: EventName,
...args: Parameters<TEventsMap[EventName]>
): void {
this.listeners?.get(eventName)?.forEach((listener) => listener(...args));
}
listenerCount<EventName extends keyof TEventsMap>(eventName: EventName): number {
return this.listeners?.get(eventName)?.size ?? 0;
}
}
export class NativeModule<TEventsMap extends Record<never, never>>
extends EventEmitter<TEventsMap>
implements NativeModuleType
{
[key: string]: any;
ViewPrototype?: object | undefined;
__expo_module_name__?: string;
}
class SharedObject<TEventsMap extends Record<never, never>>
extends EventEmitter<TEventsMap>
implements SharedObjectType
{
release(): void {
throw new Error('Method not implemented.');
}
}
globalThis.expo = {
EventEmitter,
NativeModule,
SharedObject,
modules: {},
uuidv4: uuid.v4,
uuidv5: uuid.v5,
getViewConfig: () => {
throw new Error('Method not implemented.');
},
reloadAppAsync: async () => {
window.location.reload();
},
};

View File

@@ -0,0 +1 @@
export * from './CoreModule';