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,20 @@
import { Asset } from './Asset';
import { IS_ENV_WITH_LOCAL_ASSETS } from './PlatformUtils';
import { setCustomSourceTransformer } from './resolveAssetSource';
// Override React Native's asset resolution for `Image` components in contexts where it matters
if (IS_ENV_WITH_LOCAL_ASSETS) {
setCustomSourceTransformer((resolver) => {
try {
// Bundler is using the hashAssetFiles plugin if and only if the fileHashes property exists
if (resolver.asset.fileHashes) {
const asset = Asset.fromMetadata(resolver.asset);
return resolver.fromSource(asset.downloaded ? asset.localUri! : asset.uri);
} else {
return resolver.defaultAsset();
}
} catch {
return resolver.defaultAsset();
}
});
}

View File

@@ -0,0 +1,271 @@
import { getAssetByID } from '@react-native/assets-registry/registry';
import { Platform } from 'expo-modules-core';
import { AssetMetadata, selectAssetSource } from './AssetSources';
import * as AssetUris from './AssetUris';
import { downloadAsync } from './ExpoAsset';
import * as ImageAssets from './ImageAssets';
import { getLocalAssetUri } from './LocalAssets';
import { IS_ENV_WITH_LOCAL_ASSETS } from './PlatformUtils';
import resolveAssetSource from './resolveAssetSource';
// @docsMissing
export type AssetDescriptor = {
name: string;
type: string;
hash?: string | null;
uri: string;
width?: number | null;
height?: number | null;
};
type DownloadPromiseCallbacks = {
resolve: () => void;
reject: (error: Error) => void;
};
export { AssetMetadata };
/**
* The `Asset` class represents an asset in your app. It gives metadata about the asset (such as its
* name and type) and provides facilities to load the asset data.
*/
export class Asset {
private static byHash = {};
private static byUri = {};
/**
* The name of the asset file without the extension. Also without the part from `@` onward in the
* filename (used to specify scale factor for images).
*/
public name: string;
/**
* The extension of the asset filename.
*/
public readonly type: string;
/**
* The MD5 hash of the asset's data.
*/
public readonly hash: string | null = null;
/**
* A URI that points to the asset's data on the remote server. When running the published version
* of your app, this refers to the location on Expo's asset server where Expo has stored your
* asset. When running the app from Expo CLI during development, this URI points to Expo CLI's
* server running on your computer and the asset is served directly from your computer. If you
* are not using Classic Updates (legacy), this field should be ignored as we ensure your assets
* are on device before before running your application logic.
*/
public readonly uri: string;
/**
* If the asset has been downloaded (by calling [`downloadAsync()`](#downloadasync)), the
* `file://` URI pointing to the local file on the device that contains the asset data.
*/
public localUri: string | null = null;
/**
* If the asset is an image, the width of the image data divided by the scale factor. The scale
* factor is the number after `@` in the filename, or `1` if not present.
*/
public width: number | null = null;
/**
* If the asset is an image, the height of the image data divided by the scale factor. The scale factor is the number after `@` in the filename, or `1` if not present.
*/
public height: number | null = null;
private downloading: boolean = false;
/**
* Whether the asset has finished downloading from a call to [`downloadAsync()`](#downloadasync).
*/
public downloaded: boolean = false;
private _downloadCallbacks: DownloadPromiseCallbacks[] = [];
constructor({ name, type, hash = null, uri, width, height }: AssetDescriptor) {
this.name = name;
this.type = type;
this.hash = hash;
this.uri = uri;
if (typeof width === 'number') {
this.width = width;
}
if (typeof height === 'number') {
this.height = height;
}
if (hash) {
this.localUri = getLocalAssetUri(hash, type);
if (this.localUri) {
this.downloaded = true;
}
}
if (Platform.OS === 'web') {
if (!name) {
this.name = AssetUris.getFilename(uri);
}
if (!type) {
this.type = AssetUris.getFileExtension(uri);
}
}
}
// @needsAudit
/**
* A helper that wraps `Asset.fromModule(module).downloadAsync` for convenience.
* @param moduleId An array of `require('path/to/file')` or external network URLs. Can also be
* just one module or URL without an Array.
* @return Returns a Promise that fulfills with an array of `Asset`s when the asset(s) has been
* saved to disk.
* @example
* ```ts
* const [{ localUri }] = await Asset.loadAsync(require('./assets/snack-icon.png'));
* ```
*/
static loadAsync(moduleId: number | number[] | string | string[]): Promise<Asset[]> {
const moduleIds = Array.isArray(moduleId) ? moduleId : [moduleId];
return Promise.all(moduleIds.map((moduleId) => Asset.fromModule(moduleId).downloadAsync()));
}
// @needsAudit
/**
* Returns the [`Asset`](#asset) instance representing an asset given its module or URL.
* @param virtualAssetModule The value of `require('path/to/file')` for the asset or external
* network URL
* @return The [`Asset`](#asset) instance for the asset.
*/
static fromModule(virtualAssetModule: number | string): Asset {
if (typeof virtualAssetModule === 'string') {
return Asset.fromURI(virtualAssetModule);
}
const meta = getAssetByID(virtualAssetModule);
if (!meta) {
throw new Error(`Module "${virtualAssetModule}" is missing from the asset registry`);
}
// Outside of the managed env we need the moduleId to initialize the asset
// because resolveAssetSource depends on it
if (!IS_ENV_WITH_LOCAL_ASSETS) {
// null-check is performed above with `getAssetByID`.
const { uri } = resolveAssetSource(virtualAssetModule)!;
const asset = new Asset({
name: meta.name,
type: meta.type,
hash: meta.hash,
uri,
width: meta.width,
height: meta.height,
});
// For images backward compatibility,
// keeps localUri the same as uri for React Native's Image that
// works fine with drawable resource names.
if (Platform.OS === 'android' && !uri.includes(':') && (meta.width || meta.height)) {
asset.localUri = asset.uri;
asset.downloaded = true;
}
Asset.byHash[meta.hash] = asset;
return asset;
}
return Asset.fromMetadata(meta);
}
// @docsMissing
static fromMetadata(meta: AssetMetadata): Asset {
// The hash of the whole asset, not to be confused with the hash of a specific file returned
// from `selectAssetSource`
const metaHash = meta.hash;
if (Asset.byHash[metaHash]) {
return Asset.byHash[metaHash];
}
const { uri, hash } = selectAssetSource(meta);
const asset = new Asset({
name: meta.name,
type: meta.type,
hash,
uri,
width: meta.width,
height: meta.height,
});
Asset.byHash[metaHash] = asset;
return asset;
}
// @docsMissing
static fromURI(uri: string): Asset {
if (Asset.byUri[uri]) {
return Asset.byUri[uri];
}
// Possibly a Base64-encoded URI
let type = '';
if (uri.indexOf(';base64') > -1) {
type = uri.split(';')[0].split('/')[1];
} else {
const extension = AssetUris.getFileExtension(uri);
type = extension.startsWith('.') ? extension.substring(1) : extension;
}
const asset = new Asset({
name: '',
type,
hash: null,
uri,
});
Asset.byUri[uri] = asset;
return asset;
}
// @needsAudit
/**
* Downloads the asset data to a local file in the device's cache directory. Once the returned
* promise is fulfilled without error, the [`localUri`](#localuri) field of this asset points
* to a local file containing the asset data. The asset is only downloaded if an up-to-date local
* file for the asset isn't already present due to an earlier download. The downloaded `Asset`
* will be returned when the promise is resolved.
* @return Returns a Promise which fulfills with an `Asset` instance.
*/
async downloadAsync(): Promise<this> {
if (this.downloaded) {
return this;
}
if (this.downloading) {
await new Promise<void>((resolve, reject) => {
this._downloadCallbacks.push({ resolve, reject });
});
return this;
}
this.downloading = true;
try {
if (Platform.OS === 'web') {
if (ImageAssets.isImageType(this.type)) {
const { width, height, name } = await ImageAssets.getImageInfoAsync(this.uri);
this.width = width;
this.height = height;
this.name = name;
} else {
this.name = AssetUris.getFilename(this.uri);
}
}
this.localUri = await downloadAsync(this.uri, this.hash, this.type);
this.downloaded = true;
this._downloadCallbacks.forEach(({ resolve }) => resolve());
} catch (e) {
this._downloadCallbacks.forEach(({ reject }) => reject(e));
throw e;
} finally {
this.downloading = false;
this._downloadCallbacks = [];
}
return this;
}
}

View File

@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import { Asset } from './Asset';
// @needsAudit
/**
* Downloads and stores one or more assets locally.
* After the assets are loaded, this hook returns a list of asset instances.
* If something went wrong when loading the assets, an error is returned.
*
* > Note, the assets are not "reloaded" when you dynamically change the asset list.
*
* @return Returns an array containing:
* - on the first position, a list of all loaded assets. If they aren't loaded yet, this value is
* `undefined`.
* - on the second position, an error which encountered when loading the assets. If there was no
* error, this value is `undefined`.
*
* @example
* ```tsx
* const [assets, error] = useAssets([require('path/to/asset.jpg'), require('path/to/other.png')]);
*
* return assets ? <Image source={assets[0]} /> : null;
* ```
*/
export function useAssets(moduleIds: number | number[]): [Asset[] | undefined, Error | undefined] {
const [assets, setAssets] = useState<Asset[]>();
const [error, setError] = useState<Error>();
useEffect(() => {
Asset.loadAsync(moduleIds).then(setAssets).catch(setError);
}, []);
return [assets, error];
}

View File

@@ -0,0 +1,3 @@
import AssetSourceResolver from 'react-native/Libraries/Image/AssetSourceResolver';
export default AssetSourceResolver;
export * from 'react-native/Libraries/Image/AssetSourceResolver';

View File

@@ -0,0 +1,88 @@
import type { PackagerAsset } from '@react-native/assets-registry/registry';
import { Platform } from 'expo-modules-core';
import { PixelRatio } from 'react-native';
export type ResolvedAssetSource = {
__packager_asset: boolean;
width?: number;
height?: number;
uri: string;
scale: number;
};
// Returns the Metro dev server-specific asset location.
function getScaledAssetPath(asset): string {
const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
const type = !asset.type ? '' : `.${asset.type}`;
if (__DEV__) {
return asset.httpServerLocation + '/' + asset.name + scaleSuffix + type;
} else {
return asset.httpServerLocation.replace(/\.\.\//g, '_') + '/' + asset.name + scaleSuffix + type;
}
}
export default class AssetSourceResolver {
private readonly serverUrl: string;
// where the jsbundle is being run from
// NOTE(EvanBacon): Never defined on web.
private readonly jsbundleUrl: string | undefined | null;
// the asset to resolve
public readonly asset: PackagerAsset;
constructor(
serverUrl: string | undefined | null,
jsbundleUrl: string | undefined | null,
asset: PackagerAsset
) {
this.serverUrl = serverUrl || 'https://expo.dev';
this.jsbundleUrl = null;
this.asset = asset;
}
// Always true for web runtimes
isLoadedFromServer(): boolean {
return true;
}
// Always false for web runtimes
isLoadedFromFileSystem(): boolean {
return false;
}
defaultAsset(): ResolvedAssetSource {
return this.assetServerURL();
}
/**
* @returns absolute remote URL for the hosted asset.
*/
assetServerURL(): ResolvedAssetSource {
const fromUrl = new URL(getScaledAssetPath(this.asset), this.serverUrl);
fromUrl.searchParams.set('platform', Platform.OS);
fromUrl.searchParams.set('hash', this.asset.hash);
return this.fromSource(
// Relative on web
fromUrl.toString().replace(fromUrl.origin, '')
);
}
fromSource(source: string): ResolvedAssetSource {
return {
__packager_asset: true,
width: this.asset.width ?? undefined,
height: this.asset.height ?? undefined,
uri: source,
scale: AssetSourceResolver.pickScale(this.asset.scales, PixelRatio.get()),
};
}
static pickScale(scales: number[], deviceScale: number): number {
for (let i = 0; i < scales.length; i++) {
if (scales[i] >= deviceScale) {
return scales[i];
}
}
return scales[scales.length - 1] || 1;
}
}

View File

@@ -0,0 +1,121 @@
import type { PackagerAsset } from '@react-native/assets-registry/registry';
import { Platform } from 'expo-modules-core';
import { PixelRatio, NativeModules } from 'react-native';
import AssetSourceResolver from './AssetSourceResolver';
import { getManifest2, manifestBaseUrl } from './PlatformUtils';
// @docsMissing
export type AssetMetadata = Pick<
PackagerAsset,
'httpServerLocation' | 'name' | 'hash' | 'type' | 'scales' | 'width' | 'height'
> & {
uri?: string;
fileHashes?: string[];
fileUris?: string[];
};
export type AssetSource = {
uri: string;
hash: string;
};
/**
* Selects the best file for the given asset (ex: choosing the best scale for images) and returns
* a { uri, hash } pair for the specific asset file.
*
* If the asset isn't an image with multiple scales, the first file is selected.
*/
export function selectAssetSource(meta: AssetMetadata): AssetSource {
// This logic is based on that of AssetSourceResolver, with additional support for file hashes and
// explicitly provided URIs
const scale = AssetSourceResolver.pickScale(meta.scales, PixelRatio.get());
const index = meta.scales.findIndex((s) => s === scale);
const hash = meta.fileHashes ? meta.fileHashes[index] ?? meta.fileHashes[0] : meta.hash;
// Allow asset processors to directly provide the URL to load
const uri = meta.fileUris ? meta.fileUris[index] ?? meta.fileUris[0] : meta.uri;
if (uri) {
return { uri: resolveUri(uri), hash };
}
const fileScale = scale === 1 ? '' : `@${scale}x`;
const fileExtension = meta.type ? `.${encodeURIComponent(meta.type)}` : '';
const suffix = `/${encodeURIComponent(meta.name)}${fileScale}${fileExtension}`;
const params = new URLSearchParams({
platform: Platform.OS,
hash: meta.hash,
});
// For assets with a specified absolute URL, we use the existing origin instead of prepending the
// development server or production CDN URL origin
if (/^https?:\/\//.test(meta.httpServerLocation)) {
const uri = meta.httpServerLocation + suffix + '?' + params;
return { uri, hash };
}
// For assets during development using manifest2, we use the development server's URL origin
const manifest2 = getManifest2();
const devServerUrl = manifest2?.extra?.expoGo?.developer
? 'http://' + manifest2.extra.expoGo.debuggerHost
: null;
if (devServerUrl) {
const baseUrl = new URL(meta.httpServerLocation + suffix, devServerUrl);
baseUrl.searchParams.set('platform', Platform.OS);
baseUrl.searchParams.set('hash', meta.hash);
return {
uri: baseUrl.href,
hash,
};
}
// Temporary fallback for loading assets in Expo Go home
if (NativeModules.ExponentKernel) {
return { uri: `https://classic-assets.eascdn.net/~assets/${encodeURIComponent(hash)}`, hash };
}
// In correctly configured apps, we arrive here if the asset is locally available on disk due to
// being managed by expo-updates, and `getLocalAssetUri(hash)` must return a local URI for this
// hash. Since the asset is local, we don't have a remote URL and specify an invalid URL (an empty
// string) as a placeholder.
return { uri: '', hash };
}
/**
* Resolves the given URI to an absolute URI. If the given URI is already an absolute URI, it is
* simply returned. Otherwise, if it is a relative URI, it is resolved relative to the manifest's
* base URI.
*/
export function resolveUri(uri: string): string {
// `manifestBaseUrl` is always an absolute URL or `null`.
return manifestBaseUrl ? new URL(uri, manifestBaseUrl).href : uri;
}
// A very cheap path canonicalization like path.join but without depending on a `path` polyfill.
export function pathJoin(...paths: string[]): string {
// Start by simply combining paths, without worrying about ".." or "."
const combined = paths
.map((part, index) => {
if (index === 0) {
return part.trim().replace(/\/*$/, '');
}
return part.trim().replace(/(^\/*|\/*$)/g, '');
})
.filter((part) => part.length > 0)
.join('/')
.split('/');
// Handle ".." and "." in paths
const resolved: string[] = [];
for (const part of combined) {
if (part === '..') {
resolved.pop(); // Remove the last element from the result
} else if (part !== '.') {
resolved.push(part);
}
}
return resolved.join('/');
}

View File

@@ -0,0 +1,63 @@
export function getFilename(url: string): string {
const { pathname, searchParams } = new URL(url, 'https://e');
// When attached to a dev server, we use `unstable_path` to represent the file path. This ensures
// the file name is not canonicalized by the browser.
// NOTE(EvanBacon): This is technically not tied to `__DEV__` as it's possible to use this while bundling in production
// mode.
if (__DEV__) {
if (searchParams.has('unstable_path')) {
const encodedFilePath = decodeURIComponent(searchParams.get('unstable_path')!);
return getBasename(encodedFilePath);
}
}
return getBasename(pathname);
}
function getBasename(pathname: string): string {
return pathname.substring(pathname.lastIndexOf('/') + 1);
}
export function getFileExtension(url: string): string {
const filename = getFilename(url);
const dotIndex = filename.lastIndexOf('.');
// Ignore leading dots for hidden files
return dotIndex > 0 ? filename.substring(dotIndex) : '';
}
/**
* Returns the base URL from a manifest's URL. For example, given a manifest hosted at
* https://example.com/app/manifest.json, the base URL would be https://example.com/app/. Query
* parameters and fragments also are removed.
*
* For an Expo-hosted project with a manifest hosted at https://exp.host/@user/project/index.exp, the
* base URL would be https://exp.host/@user/project.
*
* We also normalize the "exp" protocol to "http" to handle internal URLs with the Expo schemes used
* to tell the OS to open the URLs in the the Expo client.
*/
export function getManifestBaseUrl(manifestUrl: string): string {
const urlObject = new URL(manifestUrl);
let nextProtocol = urlObject.protocol;
// Change the scheme to http(s) if it is exp(s)
if (nextProtocol === 'exp:') {
nextProtocol = 'http:';
} else if (nextProtocol === 'exps:') {
nextProtocol = 'https:';
}
urlObject.protocol = nextProtocol;
// Trim filename, query parameters, and fragment, if any
const directory = urlObject.pathname.substring(0, urlObject.pathname.lastIndexOf('/') + 1);
urlObject.pathname = directory;
urlObject.search = '';
urlObject.hash = '';
// The URL spec doesn't allow for changing the protocol to `http` or `https`
// without a port set so instead, we'll just swap the protocol manually.
return urlObject.protocol !== nextProtocol
? urlObject.href.replace(urlObject.protocol, nextProtocol)
: urlObject.href;
}

View File

@@ -0,0 +1,20 @@
import { requireNativeModule } from 'expo-modules-core';
const AssetModule = requireNativeModule('ExpoAsset');
/**
* Downloads the asset from the given URL to a local cache and returns the local URL of the cached
* file.
*
* If there is already a locally cached file and its MD5 hash matches the given `md5Hash` parameter,
* if present, the remote asset is not downloaded. The `hash` property is included in Metro's asset
* metadata objects when this module's `hashAssetFiles` plugin is used, which is the typical way the
* `md5Hash` parameter of this function is provided.
*/
export async function downloadAsync(
url: string,
md5Hash: string | null,
type: string
): Promise<string> {
return AssetModule.downloadAsync(url, md5Hash, type);
}

View File

@@ -0,0 +1,7 @@
export async function downloadAsync(
url: string,
_hash: string | null,
_type: string
): Promise<string> {
return url;
}

View File

@@ -0,0 +1,32 @@
/* eslint-env browser */
import { Platform } from 'expo-modules-core';
import { getFilename } from './AssetUris';
type ImageInfo = {
name: string;
width: number;
height: number;
};
export function isImageType(type: string): boolean {
return /^(jpeg|jpg|gif|png|bmp|webp|heic)$/i.test(type);
}
export function getImageInfoAsync(url: string): Promise<ImageInfo> {
if (!Platform.isDOMAvailable) {
return Promise.resolve({ name: getFilename(url), width: 0, height: 0 });
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onerror = reject;
img.onload = () => {
resolve({
name: getFilename(url),
width: img.naturalWidth,
height: img.naturalHeight,
});
};
img.src = url;
});
}

View File

@@ -0,0 +1,24 @@
import { getLocalAssets } from './PlatformUtils';
// localAssets are provided by the expo-updates module
const localAssets = getLocalAssets();
/**
* Returns the URI of a local asset from its hash, or null if the asset is not available locally
*/
export function getLocalAssetUri(hash: string, type: string | null): string | null {
const localAssetsKey = hash;
const legacyLocalAssetsKey = `${hash}.${type ?? ''}`;
switch (true) {
case localAssetsKey in localAssets: {
return localAssets[localAssetsKey];
}
case legacyLocalAssetsKey in localAssets: {
// legacy updates store assets with an extension
return localAssets[legacyLocalAssetsKey];
}
default:
return null;
}
}

View File

@@ -0,0 +1,4 @@
export function getLocalAssetUri(hash: string, type: string | null): string | null {
// noop on web
return null;
}

View File

@@ -0,0 +1,39 @@
import Constants, { AppOwnership } from 'expo-constants';
import { requireOptionalNativeModule } from 'expo-modules-core';
// @ts-ignore -- optional interface, will gracefully degrade to `any` if not installed
import type { ExpoUpdatesModule } from 'expo-updates';
import { getManifestBaseUrl } from './AssetUris';
const ExpoUpdates = requireOptionalNativeModule<ExpoUpdatesModule>('ExpoUpdates');
const isRunningInExpoGo = Constants.appOwnership === AppOwnership.Expo;
// expo-updates (and Expo Go expo-updates override) manages assets from updates and exposes
// the ExpoUpdates.localAssets constant containing information about the assets.
const expoUpdatesIsInstalledAndEnabled = !!ExpoUpdates?.isEnabled;
const expoUpdatesIsUsingEmbeddedAssets = ExpoUpdates?.isUsingEmbeddedAssets;
// if expo-updates is installed but we're running directly from the embedded bundle, we don't want
// to override the AssetSourceResolver.
const shouldUseUpdatesAssetResolution =
expoUpdatesIsInstalledAndEnabled && !expoUpdatesIsUsingEmbeddedAssets;
// Expo Go always uses the updates module for asset resolution (local assets) since it
// overrides the expo-updates module.
export const IS_ENV_WITH_LOCAL_ASSETS = isRunningInExpoGo || shouldUseUpdatesAssetResolution;
// Get the localAssets property from the ExpoUpdates native module so that we do
// not need to include expo-updates as a dependency of expo-asset
export function getLocalAssets(): Record<string, string> {
return ExpoUpdates?.localAssets ?? {};
}
export function getManifest2(): typeof Constants.__unsafeNoWarnManifest2 {
return Constants.__unsafeNoWarnManifest2;
}
// Compute manifest base URL if available
export const manifestBaseUrl = Constants.experienceUrl
? getManifestBaseUrl(Constants.experienceUrl)
: null;

View File

@@ -0,0 +1,12 @@
export const IS_ENV_WITH_LOCAL_ASSETS = false;
export function getLocalAssets(): Record<string, string> {
return {};
}
export function getManifest2() {
return {};
}
// Compute manifest base URL if available
export const manifestBaseUrl = null;

View File

@@ -0,0 +1,4 @@
import './Asset.fx';
export * from './Asset';
export * from './AssetHooks';

View File

@@ -0,0 +1,3 @@
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
export default resolveAssetSource;
export * from 'react-native/Libraries/Image/resolveAssetSource'; // eslint-disable-line import/export

View File

@@ -0,0 +1,45 @@
import { getAssetByID } from '@react-native/assets-registry/registry';
import AssetSourceResolver, { ResolvedAssetSource } from './AssetSourceResolver';
let _customSourceTransformer: (resolver: AssetSourceResolver) => ResolvedAssetSource;
export function setCustomSourceTransformer(
transformer: (resolver: AssetSourceResolver) => ResolvedAssetSource
): void {
_customSourceTransformer = transformer;
}
/**
* `source` is either a number (opaque type returned by require('./foo.png'))
* or an `ImageSource` like { uri: '<http location || file path>' }
*/
export default function resolveAssetSource(source: any): ResolvedAssetSource | null {
if (typeof source === 'object') {
return source;
}
const asset = getAssetByID(source);
if (!asset) {
return null;
}
const resolver = new AssetSourceResolver(
// Doesn't matter since this is removed on web
'https://expo.dev',
null,
asset
);
if (_customSourceTransformer) {
return _customSourceTransformer(resolver);
}
return resolver.defaultAsset();
}
Object.defineProperty(resolveAssetSource, 'setCustomSourceTransformer', {
get() {
return setCustomSourceTransformer;
},
});
export const { pickScale } = AssetSourceResolver;

View File

@@ -0,0 +1,43 @@
declare module 'react-native/Libraries/Image/AssetSourceResolver' {
import { PackagerAsset } from '@react-native/assets/registry';
export type ResolvedAssetSource = {
__packager_asset: boolean;
width: number | null;
height: number | null;
uri: string;
scale: number;
};
export default class AssetSourceResolver {
serverUrl: string | null;
jsbundleUrl: string | null;
asset: PackagerAsset & { fileHashes?: string[] };
constructor(serverUrl: string | null, jsbundleUrl: string | null, asset: PackagerAsset);
isLoadedFromServer(): boolean;
isLoadedFromFileSystem(): boolean;
defaultAsset(): ResolvedAssetSource;
assetServerURL(): ResolvedAssetSource;
scaledAssetPath(): ResolvedAssetSource;
scaledAssetURLNearBundle(): ResolvedAssetSource;
resourceIdentifierWithoutScale(): ResolvedAssetSource;
drawableFolderInBundle(): ResolvedAssetSource;
fromSource(source: string): ResolvedAssetSource;
static pickScale(scales: number[], deviceScale: number): number;
}
}
declare module 'react-native/Libraries/Image/resolveAssetSource' {
import AssetSourceResolver, {
ResolvedAssetSource,
} from 'react-native/Libraries/Image/AssetSourceResolver';
export default function resolveAssetSource(source: any): ResolvedAssetSource | null;
export function setCustomSourceTransformer(
transformer: (resolver: AssetSourceResolver) => ResolvedAssetSource
): void;
}