Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Mobile-Expensify
15 changes: 14 additions & 1 deletion config/webpack/webpack.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment):
}),

// This allows us to interactively inspect JS bundle contents
...(process.env.ANALYZE_BUNDLE === 'true' ? [new BundleAnalyzerPlugin()] : []),
new BundleAnalyzerPlugin(),
// ...(process.env.ANALYZE_BUNDLE === 'true' ? [new BundleAnalyzerPlugin()] : []),
],
module: {
rules: [
Expand Down Expand Up @@ -352,6 +353,18 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment):
name: 'heicTo',
chunks: 'all',
},
// ExpensifyIcons chunk - separate chunk loaded eagerly for offline support
expensifyIcons: {
test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]expensify-icons\.chunk\.ts$/,
name: 'expensifyIcons',
chunks: 'all',
},
// Illustrations chunk - separate chunk loaded eagerly for offline support
illustrations: {
test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]illustrations\.chunk\.ts$/,
name: 'illustrations',
chunks: 'all',
},
// Extract all 3rd party dependencies (~75% of App) to separate js file
// This gives a more efficient caching - 3rd party deps don't change as often as main source
// When dependencies don't change webpack would produce the same js file (and content hash)
Expand Down
27 changes: 15 additions & 12 deletions src/components/ChangeWorkspaceMenuSectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import convertToLTR from '@libs/convertToLTR';
import variables from '@styles/variables';
import type {TranslationPaths} from '@src/languages/types';
import type IconAsset from '@src/types/utils/IconAsset';
import {useMemoizedLazyAsset} from '@hooks/useLazyAsset';
import Icon from './Icon';
import * as Illustrations from './Icon/Illustrations';
import {loadIllustration} from './Icon/IllustrationLoader';
import RenderHTML from './RenderHTML';

type ChangeWorkspaceMenuSection = {
Expand All @@ -18,20 +19,22 @@ type ChangeWorkspaceMenuSection = {
titleTranslationKey: TranslationPaths;
};

const changeWorkspaceMenuSections: ChangeWorkspaceMenuSection[] = [
{
icon: Illustrations.FolderOpen,
titleTranslationKey: 'iou.changePolicyEducational.reCategorize',
},
{
icon: Illustrations.Workflows,
titleTranslationKey: 'iou.changePolicyEducational.workflows',
},
];

function ChangeWorkspaceMenuSectionList() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {asset: FolderOpenIcon} = useMemoizedLazyAsset(() => loadIllustration('FolderOpen'));
const {asset: WorkflowsIcon} = useMemoizedLazyAsset(() => loadIllustration('Workflows'));

const changeWorkspaceMenuSections: ChangeWorkspaceMenuSection[] = [
{
icon: FolderOpenIcon,
titleTranslationKey: 'iou.changePolicyEducational.reCategorize',
},
{
icon: WorkflowsIcon,
titleTranslationKey: 'iou.changePolicyEducational.workflows',
},
];

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion src/components/FeatureList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type FeatureListProps = {
menuItems: FeatureListItem[];

/** The illustration to display in the header. Can be an image or a JSON object representing a Lottie animation. */
illustration: DotLottieAnimation | IconAsset;
illustration: DotLottieAnimation | IconAsset | undefined;

/** The style passed to the illustration */
illustrationStyle?: StyleProp<ViewStyle>;
Expand Down
67 changes: 67 additions & 0 deletions src/components/Icon/ExpensifyIconLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type IconAsset from '@src/types/utils/IconAsset';

type ExpensifyIconsChunk = {
getExpensifyIcon: (iconName: string) => unknown;
AVAILABLE_EXPENSIFY_ICONS: string[];
} & Record<string, IconAsset>;

type ExpensifyIconName = string;

let expensifyIconsChunk: ExpensifyIconsChunk | null = null;
let chunkLoadingPromise: Promise<ExpensifyIconsChunk> | null = null;

/**
* Load the ExpensifyIcons chunk eagerl
*/
function loadExpensifyIconsChunk(): Promise<ExpensifyIconsChunk> {
if (expensifyIconsChunk) {
return Promise.resolve(expensifyIconsChunk);
}

if (chunkLoadingPromise) {
return chunkLoadingPromise;
}

chunkLoadingPromise = import(
/* webpackChunkName: "expensifyIcons" */
/* webpackPreload: true */
'./chunks/expensify-icons.chunk'
)
.then((chunk) => {
const typedChunk = chunk as unknown as ExpensifyIconsChunk;
expensifyIconsChunk = typedChunk;
return typedChunk;
})
.catch((error) => {
chunkLoadingPromise = null; // Reset on error to allow retry
throw new Error(`Failed to load ExpensifyIcons chunk: ${String(error)}`);
});

return chunkLoadingPromise;
}

/**
* Get an ExpensifyIcon by name from the eagerly loaded chunk
* This function provides immediate access once the chunk is loaded
*/
function loadExpensifyIcon(iconName: ExpensifyIconName): Promise<{default: IconAsset}> {
return loadExpensifyIconsChunk()
.then((chunk) => {
const icon = chunk.getExpensifyIcon(iconName) as IconAsset;
if (!icon) {
throw new Error(`ExpensifyIcon "${iconName}" not found`);
}
return {default: icon};
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error(`Failed to load ExpensifyIcon: ${iconName}`, error);
throw error;
});
}

export {
loadExpensifyIcon,
};

export type {ExpensifyIconName};
73 changes: 73 additions & 0 deletions src/components/Icon/IllustrationLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type IconAsset from '@src/types/utils/IconAsset';

type IllustrationsChunk = {
getIllustration: (illustrationName: string) => unknown;
AVAILABLE_ILLUSTRATIONS: string[];
} & Record<string, IconAsset>;

type IllustrationName = string;

let illustrationsChunk: IllustrationsChunk | null = null;
let chunkLoadingPromise: Promise<IllustrationsChunk> | null = null;

/**
* Load the illustrations chunk eagerly
*/
function loadIllustrationsChunk(): Promise<IllustrationsChunk> {
if (illustrationsChunk) {
return Promise.resolve(illustrationsChunk);
}

if (chunkLoadingPromise) {
return chunkLoadingPromise;
}

chunkLoadingPromise = import(
/* webpackChunkName: "illustrations" */
/* webpackPreload: true */
'./chunks/illustrations.chunk'
)
.then((chunk) => {
// eslint-disable-next-line no-console
console.log('Loaded illustrations chunk:', chunk);
// eslint-disable-next-line no-console
console.log('Module keys:', Object.keys(chunk));
// eslint-disable-next-line no-console
console.log('getIllustration function:', chunk.getIllustration);
const typedChunk = chunk as unknown as IllustrationsChunk;
illustrationsChunk = typedChunk;
return typedChunk;
})
.catch((error) => {
chunkLoadingPromise = null; // Reset on error to allow retry
throw new Error(`Failed to load Illustrations chunk: ${String(error)}`);
});

return chunkLoadingPromise;
}

/**
* Get an Illustration by name from the eagerly loaded chunk
* This function provides immediate access once the chunk is loaded
*/
function loadIllustration(illustrationName: IllustrationName): Promise<{default: IconAsset}> {
return loadIllustrationsChunk()
.then((chunk) => {
const illustration = chunk.getIllustration(illustrationName) as IconAsset;
if (!illustration) {
throw new Error(`Illustration "${illustrationName}" not found`);
}
return {default: illustration}; // Changed to return {default: illustration}
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error(`Failed to load Illustration: ${illustrationName}`, error);
throw error;
});
}

export {
loadIllustration,
};

export type {IllustrationName};
Loading
Loading