Extension Integration
About
This guide explains how to create and register a new extension in the Extra Wallet ecosystem.
Extensions are defined using a module config (module.config.ts), which tells the app:
- what the extension is (page, block, modal, hook, config),
- which files belong to it,
- how it should be rendered,
- whether it should appear in the App Store,
- and whether it can be used in offline mode.
Once configured correctly, your extension becomes discoverable and installable like any other built-in feature.
If your extension needs to work with sensitive data (e.g., secrets, keys, passwords), delegate all sensitive logic to the signer and expose only a secure, minimal API to your extension. Do not handle raw secrets in the UI module.
Extension types
Each module represents a logical unit in the app and has a type:
page– A full standalone route (e.g., a dedicated page in the app).block– A component that embeds into a slot of a parent module (e.g., cards on a dashboard) or itself serves as a slot for other modules, or used directly as a component.modal– A floating UI.hook– Logic-only module. Registers hooks used by the runtime; no rendered UI.config– Config-only module. Used to attach build configuration; no UI and no hooks.
page,block, andmodalare visual modules.hookandconfigare logical modules used for behavior and build-time wiring.
Edge cases & build pipeline
The build pipeline parses all module.config.ts files and:
- Reads their metadata.
- Collects all referenced files.
- Copies those files into a temporary build directory according to the current build mode (online / offline, etc.).
When things get tricky
Sometimes an extension:
- needs a hook or a piece of logic to be called at the root of the app,
- or uses dynamic imports that the build script cannot statically analyze.
If you directly import such logic from a UI module, the build script might:
- accidentally pull online-only code into the offline build, or
- miss some files that are only referenced dynamically.
To handle such cases cleanly, split responsibilities:
- Use a
hookmodule for logic that should be registered at runtime. - Use a
configmodule for build-time configuration (lists of files, managers, etc.).
This keeps your UI modules lean and prevents accidental inclusion of undesired files into specific build modes.
File structure
All extension modules live under src/modules.
Typical layout:
src/
modules/
MyExtension/
module.config.ts # required
MyExtension.tsx # main component (for page/block/modal)
hooks/
useSomething.ts # sync-loaded hook (if needed)
module.config.ts rules
- Must be placed in the module folder.
- Must export a default config object.
- Visual components should be lazy-loaded via
React.lazy. - Hooks (for
hookmodules) must be imported synchronously (no lazy/dynamic import).
Module config reference
Top-level properties
| Property | Used with types | Description |
|---|---|---|
type | page · block · modal · hook · config | Required. Visual modules use 'page', 'block', or 'modal'. Logical modules use 'hook' or 'config'. |
name | All | Required. Module name. Used as an internal ID and fallback title. |
paths | All | Required. List of file paths that must be included in the build. Paths are relative to module.config.ts and must include the file extension. |
isRemoveAble | All | Required. Whether module.config.ts itself is removed from the final build. Commonly true for config-only modules, false for runtime modules. |
doesSupportOfflineMode | All | Required. Whether this module is allowed in offline mode. |
isAvailableInStore | page · block · modal | Optional. Whether the extension is visible in the App Store. Set to true for extensions. |
isDefault | page · block · modal | Optional. Whether the module is enabled by default. Third-party extensions should set this to false. |
details | page · block · modal · hook · config | Required. Metadata about the extension. See details object. |
isVisualModule | page · block · modal | Optional. Indicates module is purely for UI and can be toggled in Settings. |
comingSoon | page · block · modal | Optional. Marks an extension as "Coming soon". Intended for modules shipped by Extra Wallet developers. |
icon | page · block · modal | Optional. Path to the extension icon. Defaults to the first letter of name if not provided. |
parentSlots | page · block · modal | Optional. Declared on the parent module to define available injection points (slots). targetSlot of child modules must match one of these. |
isToggleable | page · block · modal | Optional. Whether the extension can be enabled/disabled in the App Store. Should be true for all third-party extensions. |
components | page · block · modal | Required. Array of component configurations that describe how the module renders and where. See components[] array. |
route | page | Required. Route path of the page (e.g., '/portfolio'). |
hooks | hook | Required. Array of hooks to register at runtime. |
details object
Metadata that describes who built the module and how it should appear in the App Store.
| Property | Used with | Description |
|---|---|---|
developer | All modules | Required. Developer or organization name. For third-party extensions, must not be "MTW Administrator". |
title | Extensions | Required. Name shown in the App Store and the extension details UI. |
description | Extensions | Required. Short explanation of what the extension does. |
tags | Extensions | Required. Array of tags. Currently allowed values: 'chains', 'payment', 'secretPhrase'. |
routeId | Extensions | Required. Used to link to the extension’s details page. |
components[] array
Each entry describes how and where a visual module is rendered.
| Property | Used with types | Description |
|---|---|---|
component | page · block · modal | Required. The React component to render, lazy-loaded. |
initModuleEffects | page · block · modal | Optional. A function to register module side effects (e.g., adding items to the sidebar) when the module is initialized. |
classMap | page · block · modal | Optional. Class names passed down to the module component for layout variations. |
destination | block | Required for injected modules. Name of the parent module this module should be injected into. |
targetSlot | block | Required for injected modules. Slot name in the parent module where this module should be rendered. Must match one of the target parent's parentSlots. |
parentStyle | block | Optional. Styles for the parent slot container. |
order | block | Required when multiple blocks share the same targetSlot. Determines the render order (lower runs first). |
Example: adding an item to the sidebar
initModuleEffects: () => {
useSidebarStore.getState().addSidebarItem({
label: "App Store",
mobileLabel: "Store",
icon: StoreIcon,
order: 10,
onClick: () => redirectTo(AppRoute.APP_STORE),
isActiveTab: () => window.location.pathname === AppRoute.APP_STORE,
});
};
Checklist for custom modules
Use this checklist before shipping your extension:
-
✅
nameis set and unique. -
✅
typeis correct ('page','block','modal','hook', or'config'). -
✅
doesSupportOfflineModeis set (true/false) according to your module’s behavior. -
✅
pathsincludes all main files for your module (correct relative paths, with extensions). -
✅
isRemoveAbleis set appropriately:- usually
falsefor runtime modules, - can be
truefor pureconfigmodules.
- usually
-
✅
isToggleableistruefor third-party extensions. -
✅
isAvailableInStoreistruefor extensions. -
✅
isDefaultis set tofalsefor third-party extensions. -
✅
parentSlotsis defined on parent modules that host blocks. -
✅ For blocks embedded into parent modules:
destinationmatches the parent module’sname.targetSlotmatches one of the parent’sparentSlotsvalues.orderis set when multiple blocks share the sametargetSlot.
-
✅
details.developeris not'MTW Administrator'for third-party modules. -
✅
details.title,description,tags, androuteIdare set for extensions. -
✅
routeis set forpagemodules. -
✅ Components are lazy-loaded; Hooks are imported synchronously.
-
✅ Side-effects extension logic lives inside
initModuleEffects(or hooks) instead of top-level module code.
Example configs
1. Page module example
import { lazy } from "react";
import type { ModuleConfig } from "@/libs/types/module/module";
import { AppRoute } from "../../routes/appRoute";
const portfolioModule: ModuleConfig = {
name: "Portfolio",
type: "page",
isAvailableInStore: false,
doesSupportOfflineMode: false,
details: {
developer: "MTW Administrator",
},
route: AppRoute.ROOT, // e.g. '/'
parentSlots: {
slot1: "portfolio-slot-1",
slot2: "portfolio-slot-2",
slot3: "portfolio-slot-3",
slot4: "portfolio-slot-4",
slot5: "portfolio-slot-5",
},
components: [
{
component: lazy(() => import("./Portfolio")),
},
],
isRemoveAble: false,
paths: ["./Portfolio.tsx"],
};
export default portfolioModule;
2. Block module example
import { lazy } from "react";
import type { ModuleConfig } from "@/libs/types/module/module";
const mainPageListModule: ModuleConfig = {
name: "MainPageList",
type: "block",
isToggleable: false,
doesSupportOfflineMode: false,
isAvailableInStore: false,
isVisualModule: true,
isDefault: true,
details: {
title: "Main Page List",
description: "Shows selected assets along with key details and available actions.",
developer: "MTW Administrator",
tags: ["payment"],
routeId: "main-page-list",
},
isRemoveAble: false,
components: [
{
component: lazy(() => import("./MainPageList")),
destination: "Portfolio",
targetSlot: "portfolio-slot-3",
order: 20,
},
{
component: lazy(() => import("./MainPageList")),
destination: "Wallet",
targetSlot: "wallet-slot-3",
order: 20,
},
],
paths: ["./MainPageList.tsx"],
};
export default mainPageListModule;
3. Modal module example
import { lazy } from "react";
import type { ModuleConfig } from "@/libs/types/module/module";
const advancedSettingsModal: ModuleConfig = {
name: "AdvancedSettingsModal",
type: "modal",
details: {
developer: "MTW Administrator",
},
doesSupportOfflineMode: true,
isRemoveAble: false,
components: [
{
component: lazy(() => import("./AdvancedSettingsModal")),
},
],
paths: ["./AdvancedSettingsModal.tsx"],
};
export default advancedSettingsModal;
4. Hook module example
import type { HookModule } from "@/libs/types/module/hookModule";
import { useNotificationsSync } from "./useNotificationsSync";
const notificationsModule: HookModule = {
name: "useNotificationsSync",
type: "hook",
details: {
developer: "MTW Administrator",
},
doesSupportOfflineMode: false,
paths: ["./useNotificationsSync.ts"],
hooks: [useNotificationsSync],
isRemoveAble: false,
};
export default notificationsModule;
5. Config module example
config modules are ideal when the build script can’t parse imports.
import type { ConfigModule } from "@/libs/types/module/configModule";
const transactionManagersModule: ConfigModule = {
name: "transactionManager",
type: "config",
doesSupportOfflineMode: false,
isRemoveAble: true,
details: {
developer: "MTW Administrator",
},
paths: [
"./aptos/transactionManager.ts",
"./bitcoincash/transactionManager.ts",
"./bitcoinFamily/getBitcoinFamilyClass.ts",
"./cardano/transactionManager.ts",
"./evmFamily/transactionManager.ts",
"./polkadot/transactionManager.ts",
"./solana/transactionManager.ts",
"./sui/transactionManager.ts",
"./toncoin/transactionManager.ts",
"./tron/transactionManager.ts",
"./xrp/transactionManager.ts",
],
};
export default transactionManagersModule;
Common pitfalls
Stale service worker cache
During development or testing, you may see old UI or modules even after a fresh build. This is often caused by a cached service worker.
Symptoms
- Latest UI or module changes don’t appear.
- Application behaves inconsistently between builds or devices.
How to fix
- Open DevTools (
Ctrl/Cmd + Shift + Ior right-click → Inspect). - Go to the Application tab.
- In the sidebar, select Service Workers under the Application section.
- Click Unregister for all service workers.
- Under Cache Storage, delete all caches.
- Hard-refresh the page (
Ctrl/Cmd + Shift + R).
Missing or incorrect paths
If a module file isn’t copied into the build, verify that:
- The path in
pathsis correct and relative tomodule.config.ts. - The file extension is included.
Final notes
- All modules are parsed at build time.
- Missing required fields or invalid configuration will prevent your module from being loaded or might break specific build modes.
- Keep UI, logic, and configuration separated via
page/block/modalvs.hookvs.configmodules for clean, maintainable extensions.
For issues, feature requests, or contributions, please reach out to the core Extra Wallet development team or open a PR in the relevant repository.