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,2 @@
import { requireNativeModule } from 'expo-modules-core';
export default requireNativeModule('ExpoFontLoader');

View File

@@ -0,0 +1,219 @@
import { CodedError, Platform } from 'expo-modules-core';
import FontObserver from 'fontfaceobserver';
import { UnloadFontOptions } from './Font';
import { FontDisplay, FontResource } from './Font.types';
function getFontFaceStyleSheet(): CSSStyleSheet | null {
if (!Platform.isDOMAvailable) {
return null;
}
const styleSheet = getStyleElement();
return styleSheet.sheet ? (styleSheet.sheet as CSSStyleSheet) : null;
}
type RuleItem = { rule: CSSFontFaceRule; index: number };
function getFontFaceRules(): RuleItem[] {
const sheet = getFontFaceStyleSheet();
if (sheet) {
// @ts-ignore: rule iterator
const rules = [...sheet.cssRules];
const items: RuleItem[] = [];
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
if (rule instanceof CSSFontFaceRule) {
items.push({ rule, index: i });
}
}
return items;
}
return [];
}
function getFontFaceRulesMatchingResource(
fontFamilyName: string,
options?: UnloadFontOptions
): RuleItem[] {
const rules = getFontFaceRules();
return rules.filter(({ rule }) => {
return (
rule.style.fontFamily === fontFamilyName &&
(options && options.display ? options.display === (rule.style as any).fontDisplay : true)
);
});
}
const serverContext: Set<{ name: string; css: string; resourceId: string }> = new Set();
function getHeadElements(): {
$$type: string;
rel?: string;
href?: string;
as?: string;
crossorigin?: string;
children?: string;
id?: string;
type?: string;
}[] {
const entries = [...serverContext.entries()];
if (!entries.length) {
return [];
}
const css = entries.map(([{ css }]) => css).join('\n');
const links = entries.map(([{ resourceId }]) => resourceId);
// TODO: Maybe return nothing if no fonts were loaded.
return [
{
$$type: 'style',
children: css,
id: ID,
type: 'text/css',
},
...links.map((resourceId) => ({
$$type: 'link',
rel: 'preload',
href: resourceId,
as: 'font',
crossorigin: '',
})),
];
}
export default {
async unloadAllAsync(): Promise<void> {
if (!Platform.isDOMAvailable) return;
const element = document.getElementById(ID);
if (element && element instanceof HTMLStyleElement) {
document.removeChild(element);
}
},
async unloadAsync(fontFamilyName: string, options?: UnloadFontOptions): Promise<void> {
const sheet = getFontFaceStyleSheet();
if (!sheet) return;
const items = getFontFaceRulesMatchingResource(fontFamilyName, options);
for (const item of items) {
sheet.deleteRule(item.index);
}
},
getServerResources(): string[] {
const elements = getHeadElements();
return elements
.map((element) => {
switch (element.$$type) {
case 'style':
return `<style id="${element.id}" type="${element.type}">${element.children}</style>`;
case 'link':
return `<link rel="${element.rel}" href="${element.href}" as="${element.as}" crossorigin="${element.crossorigin}" />`;
default:
return '';
}
})
.filter(Boolean);
},
resetServerContext() {
serverContext.clear();
},
isLoaded(fontFamilyName: string, resource: UnloadFontOptions = {}): boolean {
if (typeof window === 'undefined') {
return !![...serverContext.values()].find((asset) => {
return asset.name === fontFamilyName;
});
}
return getFontFaceRulesMatchingResource(fontFamilyName, resource)?.length > 0;
},
// NOTE(EvanBacon): No async keyword! This cannot return a promise in Node environments.
loadAsync(fontFamilyName: string, resource: FontResource): Promise<void> {
if (typeof window === 'undefined') {
serverContext.add({
name: fontFamilyName,
css: _createWebFontTemplate(fontFamilyName, resource),
// @ts-expect-error: typeof string
resourceId: resource.uri!,
});
return Promise.resolve();
}
const canInjectStyle = document.head && typeof document.head.appendChild === 'function';
if (!canInjectStyle) {
throw new CodedError(
'ERR_WEB_ENVIRONMENT',
`The browser's \`document.head\` element doesn't support injecting fonts.`
);
}
const style = getStyleElement();
document.head!.appendChild(style);
const res = getFontFaceRulesMatchingResource(fontFamilyName, resource);
if (!res.length) {
_createWebStyle(fontFamilyName, resource);
}
if (!isFontLoadingListenerSupported()) {
return Promise.resolve();
}
return new FontObserver(fontFamilyName, { display: resource.display }).load(null, 6000);
},
};
const ID = 'expo-generated-fonts';
function getStyleElement(): HTMLStyleElement {
const element = document.getElementById(ID);
if (element && element instanceof HTMLStyleElement) {
return element;
}
const styleElement = document.createElement('style');
styleElement.id = ID;
styleElement.type = 'text/css';
return styleElement;
}
export function _createWebFontTemplate(fontFamily: string, resource: FontResource): string {
return `@font-face{font-family:${fontFamily};src:url(${resource.uri});font-display:${
resource.display || FontDisplay.AUTO
}}`;
}
function _createWebStyle(fontFamily: string, resource: FontResource): HTMLStyleElement {
const fontStyle = _createWebFontTemplate(fontFamily, resource);
const styleElement = getStyleElement();
// @ts-ignore: TypeScript does not define HTMLStyleElement::styleSheet. This is just for IE and
// possibly can be removed if it's unnecessary on IE 11.
if (styleElement.styleSheet) {
const styleElementIE = styleElement as any;
styleElementIE.styleSheet.cssText = styleElementIE.styleSheet.cssText
? styleElementIE.styleSheet.cssText + fontStyle
: fontStyle;
} else {
const textNode = document.createTextNode(fontStyle);
styleElement.appendChild(textNode);
}
return styleElement;
}
function isFontLoadingListenerSupported(): boolean {
const { userAgent } = window.navigator;
// WebKit is broken https://github.com/bramstein/fontfaceobserver/issues/95
const isIOS = !!userAgent.match(/iPad|iPhone/i);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// Edge is broken https://github.com/bramstein/fontfaceobserver/issues/109#issuecomment-333356795
const isEdge = userAgent.includes('Edge');
// Internet Explorer
const isIE = userAgent.includes('Trident');
// Firefox
const isFirefox = userAgent.includes('Firefox');
return !isSafari && !isIOS && !isEdge && !isIE && !isFirefox;
}

View File

@@ -0,0 +1,239 @@
import { CodedError, Platform, UnavailabilityError } from 'expo-modules-core';
import ExpoFontLoader from './ExpoFontLoader';
import { FontDisplay, FontSource, FontResource, UnloadFontOptions } from './Font.types';
import {
getAssetForSource,
loadSingleFontAsync,
fontFamilyNeedsScoping,
getNativeFontName,
} from './FontLoader';
import { loaded, loadPromises } from './memory';
import { registerStaticFont } from './server';
// @needsAudit
// note(brentvatne): at some point we may want to warn if this is called outside of a managed app.
/**
* Used to transform font family names to the scoped name. This does not need to
* be called in standalone or bare apps but it will return unscoped font family
* names if it is called in those contexts.
*
* @param fontFamily Name of font to process.
* @returns Returns a name processed for use with the [current workflow](https://docs.expo.dev/archive/managed-vs-bare/).
*/
export function processFontFamily(fontFamily: string | null): string | null {
if (!fontFamily || !fontFamilyNeedsScoping(fontFamily)) {
return fontFamily;
}
if (!isLoaded(fontFamily)) {
if (__DEV__) {
if (isLoading(fontFamily)) {
console.warn(
`You started loading the font "${fontFamily}", but used it before it finished loading. You need to wait for Font.loadAsync to complete before using the font.`
);
} else {
console.warn(
`fontFamily "${fontFamily}" is not a system font and has not been loaded through expo-font.`
);
}
}
}
return `ExpoFont-${getNativeFontName(fontFamily)}`;
}
// @needsAudit
/**
* Synchronously detect if the font for `fontFamily` has finished loading.
*
* @param fontFamily The name used to load the `FontResource`.
* @return Returns `true` if the font has fully loaded.
*/
export function isLoaded(fontFamily: string): boolean {
if (Platform.OS === 'web') {
return fontFamily in loaded || !!ExpoFontLoader.isLoaded(fontFamily);
}
return fontFamily in loaded || ExpoFontLoader.customNativeFonts?.includes(fontFamily);
}
// @needsAudit
/**
* Synchronously detect if the font for `fontFamily` is still being loaded.
*
* @param fontFamily The name used to load the `FontResource`.
* @returns Returns `true` if the font is still loading.
*/
export function isLoading(fontFamily: string): boolean {
return fontFamily in loadPromises;
}
// @needsAudit
/**
* Highly efficient method for loading fonts from static or remote resources which can then be used
* with the platform's native text elements. In the browser this generates a `@font-face` block in
* a shared style sheet for fonts. No CSS is needed to use this method.
*
* @param fontFamilyOrFontMap string or map of values that can be used as the [`fontFamily`](https://reactnative.dev/docs/text#style)
* style prop with React Native Text elements.
* @param source the font asset that should be loaded into the `fontFamily` namespace.
*
* @return Returns a promise that fulfils when the font has loaded. Often you may want to wrap the
* method in a `try/catch/finally` to ensure the app continues if the font fails to load.
*/
export function loadAsync(
fontFamilyOrFontMap: string | Record<string, FontSource>,
source?: FontSource
): Promise<void> {
// NOTE(EvanBacon): Static render pass on web must be synchronous to collect all fonts.
// Because of this, `loadAsync` doesn't use the `async` keyword and deviates from the
// standard Expo SDK style guide.
const isServer = Platform.OS === 'web' && typeof window === 'undefined';
if (typeof fontFamilyOrFontMap === 'object') {
if (source) {
return Promise.reject(
new CodedError(
`ERR_FONT_API`,
`No fontFamily can be used for the provided source: ${source}. The second argument of \`loadAsync()\` can only be used with a \`string\` value as the first argument.`
)
);
}
const fontMap = fontFamilyOrFontMap;
const names = Object.keys(fontMap);
if (isServer) {
names.map((name) => registerStaticFont(name, fontMap[name]));
return Promise.resolve();
}
return Promise.all(names.map((name) => loadFontInNamespaceAsync(name, fontMap[name]))).then(
() => {}
);
}
if (isServer) {
registerStaticFont(fontFamilyOrFontMap, source);
return Promise.resolve();
}
return loadFontInNamespaceAsync(fontFamilyOrFontMap, source);
}
async function loadFontInNamespaceAsync(
fontFamily: string,
source?: FontSource | null
): Promise<void> {
if (!source) {
throw new CodedError(
`ERR_FONT_SOURCE`,
`Cannot load null or undefined font source: { "${fontFamily}": ${source} }. Expected asset of type \`FontSource\` for fontFamily of name: "${fontFamily}"`
);
}
if (loaded[fontFamily]) {
return;
}
if (loadPromises.hasOwnProperty(fontFamily)) {
return loadPromises[fontFamily];
}
// Important: we want all callers that concurrently try to load the same font to await the same
// promise. If we're here, we haven't created the promise yet. To ensure we create only one
// promise in the program, we need to create the promise synchronously without yielding the event
// loop from this point.
const asset = getAssetForSource(source);
loadPromises[fontFamily] = (async () => {
try {
await loadSingleFontAsync(fontFamily, asset);
loaded[fontFamily] = true;
} finally {
delete loadPromises[fontFamily];
}
})();
await loadPromises[fontFamily];
}
// @needsAudit
/**
* Unloads all the custom fonts. This is used for testing.
*/
export async function unloadAllAsync(): Promise<void> {
if (!ExpoFontLoader.unloadAllAsync) {
throw new UnavailabilityError('expo-font', 'unloadAllAsync');
}
if (Object.keys(loadPromises).length) {
throw new CodedError(
`ERR_UNLOAD`,
`Cannot unload fonts while they're still loading: ${Object.keys(loadPromises).join(', ')}`
);
}
for (const fontFamily of Object.keys(loaded)) {
delete loaded[fontFamily];
}
await ExpoFontLoader.unloadAllAsync();
}
// @needsAudit
/**
* Unload custom fonts matching the `fontFamily`s and display values provided.
* Because fonts are automatically unloaded on every platform this is mostly used for testing.
*
* @param fontFamilyOrFontMap The name or names of the custom fonts that will be unloaded.
* @param options When `fontFamilyOrFontMap` is a string, this should be the font source used to load
* the custom font originally.
*/
export async function unloadAsync(
fontFamilyOrFontMap: string | Record<string, UnloadFontOptions>,
options?: UnloadFontOptions
): Promise<void> {
if (!ExpoFontLoader.unloadAsync) {
throw new UnavailabilityError('expo-font', 'unloadAsync');
}
if (typeof fontFamilyOrFontMap === 'object') {
if (options) {
throw new CodedError(
`ERR_FONT_API`,
`No fontFamily can be used for the provided options: ${options}. The second argument of \`unloadAsync()\` can only be used with a \`string\` value as the first argument.`
);
}
const fontMap = fontFamilyOrFontMap;
const names = Object.keys(fontMap);
await Promise.all(names.map((name) => unloadFontInNamespaceAsync(name, fontMap[name])));
return;
}
return await unloadFontInNamespaceAsync(fontFamilyOrFontMap, options);
}
async function unloadFontInNamespaceAsync(
fontFamily: string,
options?: UnloadFontOptions | null
): Promise<void> {
if (!loaded[fontFamily]) {
return;
} else {
delete loaded[fontFamily];
}
// Important: we want all callers that concurrently try to load the same font to await the same
// promise. If we're here, we haven't created the promise yet. To ensure we create only one
// promise in the program, we need to create the promise synchronously without yielding the event
// loop from this point.
const nativeFontName = getNativeFontName(fontFamily);
if (!nativeFontName) {
throw new CodedError(`ERR_FONT_FAMILY`, `Cannot unload an empty name`);
}
await ExpoFontLoader.unloadAsync(nativeFontName, options);
}
export { FontDisplay, FontSource, FontResource, UnloadFontOptions };

View File

@@ -0,0 +1,72 @@
import { Asset } from 'expo-asset';
// @needsAudit
/**
* The different types of assets you can provide to the [`loadAsync()`](#loadasyncfontfamilyorfontmap-source) function.
* A font source can be a URI, a module ID, or an Expo Asset.
*/
export type FontSource = string | number | Asset | FontResource;
// @needsAudit
/**
* An object used to dictate the resource that is loaded into the provided font namespace when used
* with [`loadAsync`](#loadasyncfontfamilyorfontmap-source).
*/
export type FontResource = {
uri?: string | number;
/**
* Sets the [`font-display`](#fontdisplay) property for a given typeface in the browser.
* @platform web
*/
display?: FontDisplay;
default?: string;
};
// @needsAudit
/**
* Sets the [font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)
* for a given typeface. The default font value on web is `FontDisplay.AUTO`.
* Even though setting the `fontDisplay` does nothing on native platforms, the default behavior
* emulates `FontDisplay.SWAP` on flagship devices like iOS, Samsung, Pixel, etc. Default
* functionality varies on One Plus devices. In the browser this value is set in the generated
* `@font-face` CSS block and not as a style property meaning you cannot dynamically change this
* value based on the element it's used in.
* @platform web
*/
export enum FontDisplay {
/**
* __(Default)__ The font display strategy is defined by the user agent or platform.
* This generally defaults to the text being invisible until the font is loaded.
* Good for buttons or banners that require a specific treatment.
*/
AUTO = 'auto',
/**
* Fallback text is rendered immediately with a default font while the desired font is loaded.
* This is good for making the content appear to load instantly and is usually preferred.
*/
SWAP = 'swap',
/**
* The text will be invisible until the font has loaded. If the font fails to load then nothing
* will appear - it's best to turn this off when debugging missing text.
*/
BLOCK = 'block',
/**
* Splits the behavior between `SWAP` and `BLOCK`.
* There will be a [100ms timeout](https://developers.google.com/web/updates/2016/02/font-display?hl=en)
* where the text with a custom font is invisible, after that the text will either swap to the
* styled text or it'll show the unstyled text and continue to load the custom font. This is good
* for buttons that need a custom font but should also be quickly available to screen-readers.
*/
FALLBACK = 'fallback',
/**
* This works almost identically to `FALLBACK`, the only difference is that the browser will
* decide to load the font based on slow connection speed or critical resource demand.
*/
OPTIONAL = 'optional',
}
// @needsAudit
/**
* Object used to query fonts for unloading.
*/
export type UnloadFontOptions = Pick<FontResource, 'display'>;

View File

@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
import { loadAsync, isLoaded } from './Font';
import { FontSource } from './Font.types';
function isMapLoaded(map: string | Record<string, FontSource>) {
if (typeof map === 'string') {
return isLoaded(map);
} else {
return Object.keys(map).every((fontFamily) => isLoaded(fontFamily));
}
}
function useRuntimeFonts(map: string | Record<string, FontSource>): [boolean, Error | null] {
const [loaded, setLoaded] = useState(
// For web rehydration, we need to check if the fonts are already loaded during the static render.
// Native will also benefit from this optimization.
isMapLoaded(map)
);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
loadAsync(map)
.then(() => setLoaded(true))
.catch(setError);
}, []);
return [loaded, error];
}
function useStaticFonts(map: string | Record<string, FontSource>): [boolean, Error | null] {
loadAsync(map);
return [true, null];
}
// @needsAudit
/**
* ```ts
* const [loaded, error] = useFonts({ ... });
* ```
* Load a map of fonts with [`loadAsync`](#loadasyncfontfamilyorfontmap-source). This returns a `boolean` if the fonts are
* loaded and ready to use. It also returns an error if something went wrong, to use in development.
*
* > Note, the fonts are not "reloaded" when you dynamically change the font map.
*
* @param map A map of `fontFamily`s to [`FontSource`](#fontsource)s. After loading the font you can
* use the key in the `fontFamily` style prop of a `Text` element.
*
* @return
* - __loaded__ (`boolean`) - A boolean to detect if the font for `fontFamily` has finished
* loading.
* - __error__ (`Error | null`) - An error encountered when loading the fonts.
*/
export const useFonts: (map: string | Record<string, FontSource>) => [boolean, Error | null] =
typeof window === 'undefined' ? useStaticFonts : useRuntimeFonts;

View File

@@ -0,0 +1,64 @@
import { Asset } from 'expo-asset';
import Constants from 'expo-constants';
import { CodedError, Platform } from 'expo-modules-core';
import ExpoFontLoader from './ExpoFontLoader';
import { FontResource, FontSource } from './Font.types';
const isInExpoGo = Constants.appOwnership === 'expo';
export function fontFamilyNeedsScoping(name: string): boolean {
return (
isInExpoGo &&
Platform.OS !== 'ios' &&
!Constants.systemFonts.includes(name) &&
name !== 'System' &&
!name.includes(Constants.sessionId)
);
}
export function getAssetForSource(source: FontSource): Asset | FontResource {
if (source instanceof Asset) {
return source;
}
if (typeof source === 'string') {
return Asset.fromURI(source);
} else if (typeof source === 'number') {
return Asset.fromModule(source);
} else if (typeof source === 'object' && typeof source.uri !== 'undefined') {
return getAssetForSource(source.uri);
}
// @ts-ignore Error: Type 'string' is not assignable to type 'Asset'
// We can't have a string here, we would have thrown an error if !isWeb
// or returned Asset.fromModule if isWeb.
return source;
}
export async function loadSingleFontAsync(
name: string,
input: Asset | FontResource
): Promise<void> {
const asset = input as Asset;
if (!asset.downloadAsync) {
throw new CodedError(
`ERR_FONT_SOURCE`,
'`loadSingleFontAsync` expected resource of type `Asset` from expo-asset on native'
);
}
await asset.downloadAsync();
if (!asset.downloaded) {
throw new CodedError(`ERR_DOWNLOAD`, `Failed to download asset for font "${name}"`);
}
await ExpoFontLoader.loadAsync(getNativeFontName(name), asset.localUri);
}
export function getNativeFontName(name: string): string {
if (fontFamilyNeedsScoping(name)) {
return `${Constants.sessionId}-${name}`;
} else {
return name;
}
}

View File

@@ -0,0 +1,66 @@
import { Asset } from 'expo-asset';
import { CodedError } from 'expo-modules-core';
import ExpoFontLoader from './ExpoFontLoader';
import { FontResource, FontSource, FontDisplay } from './Font.types';
function uriFromFontSource(asset: any): string | null {
if (typeof asset === 'string') {
return asset || null;
} else if (typeof asset === 'object') {
return asset.uri || asset.localUri || asset.default || null;
} else if (typeof asset === 'number') {
return uriFromFontSource(Asset.fromModule(asset));
}
return null;
}
function displayFromFontSource(asset: any): FontDisplay | undefined {
return asset.display || FontDisplay.AUTO;
}
export function fontFamilyNeedsScoping(name: string): boolean {
return false;
}
export function getAssetForSource(source: FontSource): Asset | FontResource {
const uri = uriFromFontSource(source);
const display = displayFromFontSource(source);
if (!uri || typeof uri !== 'string') {
throwInvalidSourceError(uri);
}
return {
uri: uri!,
display,
};
}
function throwInvalidSourceError(source: any): never {
let type: string = typeof source;
if (type === 'object') type = JSON.stringify(source, null, 2);
throw new CodedError(
`ERR_FONT_SOURCE`,
`Expected font asset of type \`string | FontResource | Asset\` instead got: ${type}`
);
}
// NOTE(EvanBacon): No async keyword!
export function loadSingleFontAsync(name: string, input: Asset | FontResource): Promise<void> {
if (typeof input !== 'object' || typeof input.uri !== 'string' || (input as any).downloadAsync) {
throwInvalidSourceError(input);
}
try {
return ExpoFontLoader.loadAsync(name, input);
} catch {
// No-op.
}
return Promise.resolve();
}
export function getNativeFontName(name: string): string {
return name;
}

View File

@@ -0,0 +1,2 @@
export * from './Font';
export { useFonts } from './FontHooks';

View File

@@ -0,0 +1,2 @@
export const loaded: { [name: string]: boolean } = {};
export const loadPromises: { [name: string]: Promise<void> } = {};

View File

@@ -0,0 +1,34 @@
import { CodedError } from 'expo-modules-core';
import ExpoFontLoader from './ExpoFontLoader';
import { FontSource } from './Font.types';
import { getAssetForSource, loadSingleFontAsync } from './FontLoader';
/**
* @returns the server resources that should be statically extracted.
* @private
*/
export function getServerResources(): string[] {
return ExpoFontLoader.getServerResources();
}
/**
* @returns clear the server resources from the global scope.
* @private
*/
export function resetServerContext() {
return ExpoFontLoader.resetServerContext();
}
export function registerStaticFont(fontFamily: string, source?: FontSource | null) {
// MUST BE A SYNC FUNCTION!
if (!source) {
throw new CodedError(
`ERR_FONT_SOURCE`,
`Cannot load null or undefined font source: { "${fontFamily}": ${source} }. Expected asset of type \`FontSource\` for fontFamily of name: "${fontFamily}"`
);
}
const asset = getAssetForSource(source);
loadSingleFontAsync(fontFamily, asset);
}