App Integration
About
This guide explains how to create and register a new app in the EXTRA WALLET ecosystem.
Apps are defined using a module config (module.config.ts), which tells the app:
- what the app is (page, block, modal, hook, config),
- which files belong to it,
- how it should be rendered,
- whether it should appear in the
Apps, - and whether it can be used in offline mode.
Once configured correctly, your app becomes discoverable and installable like any other built-in feature.
If your app 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 app. Do not handle raw secrets in the UI module.
App 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 app:
- 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 app modules live under src/modules.
Typical layout:
src/
modules/
MyApp/
module.config.ts # required
MyApp.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 app is visible in Apps. Set to true for apps. |
isDefault | page · block · modal | Optional. Whether the module is enabled by default. Third-party apps should set this to false. |
details | page · block · modal · hook · config | Required. Metadata about the app. 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 app as "Coming soon". Intended for modules shipped by EXTRA WALLET developers. |
icon | page · block · modal | Optional. Path to the app 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 app can be enabled/disabled in Apps. Should be true for all third-party apps. |
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 Apps.
| Property | Used with | Description |
|---|---|---|
developer | All modules | Required. Developer or organization name. For third-party apps, must not be "EW Administrator". |
title | Apps | Required. Name shown in Apps and the app details UI. |
description | Apps | Required. Short explanation of what the app does. |
tags | Apps | Required. Array of tags. Currently allowed values: 'chains', 'payment', 'secretPhrase'. |
routeId | Apps | Required. Used to link to the app 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: "Apps",
mobileLabel: "Apps",
icon: StoreIcon,
order: 10,
onClick: () => redirectTo(AppRoute.APP_STORE),
isActiveTab: () => window.location.pathname === AppRoute.APP_STORE,
});
};
Checklist for custom apps
Use this checklist before shipping your app:
-
✅
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 apps. -
✅
isAvailableInStoreistruefor apps. -
✅
isDefaultis set tofalsefor third-party apps. -
✅
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'EW Administrator'for third-party apps. -
✅
details.title,description,tags, androuteIdare set for apps. -
✅
routeis set forpagemodules. -
✅ Components are lazy-loaded; Hooks are imported synchronously.
-
✅ Side-effects app 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: "EW 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: "EW 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: "EW 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: "EW 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: "EW 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 apps.
For issues, feature requests, or contributions, please reach out to the core EXTRA WALLET development team or open a PR in the relevant repository.