Skip to main content

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.

Security note

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.
Visual vs. logical modules
  • page, block, and modal are visual modules.
  • hook and config are logical modules used for behavior and build-time wiring.

Edge cases & build pipeline

The build pipeline parses all module.config.ts files and:

  1. Reads their metadata.
  2. Collects all referenced files.
  3. 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 hook module for logic that should be registered at runtime.
  • Use a config module 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 hook modules) must be imported synchronously (no lazy/dynamic import).

Module config reference

Top-level properties

PropertyUsed with typesDescription
typepage · block · modal · hook · configRequired. Visual modules use 'page', 'block', or 'modal'. Logical modules use 'hook' or 'config'.
nameAllRequired. Module name. Used as an internal ID and fallback title.
pathsAllRequired. List of file paths that must be included in the build. Paths are relative to module.config.ts and must include the file extension.
isRemoveAbleAllRequired. Whether module.config.ts itself is removed from the final build. Commonly true for config-only modules, false for runtime modules.
doesSupportOfflineModeAllRequired. Whether this module is allowed in offline mode.
isAvailableInStorepage · block · modalOptional. Whether the app is visible in Apps. Set to true for apps.
isDefaultpage · block · modalOptional. Whether the module is enabled by default. Third-party apps should set this to false.
detailspage · block · modal · hook · configRequired. Metadata about the app. See details object.
isVisualModulepage · block · modalOptional. Indicates module is purely for UI and can be toggled in Settings.
comingSoonpage · block · modalOptional. Marks an app as "Coming soon". Intended for modules shipped by EXTRA WALLET developers.
iconpage · block · modalOptional. Path to the app icon. Defaults to the first letter of name if not provided.
parentSlotspage · block · modalOptional. Declared on the parent module to define available injection points (slots). targetSlot of child modules must match one of these.
isToggleablepage · block · modalOptional. Whether the app can be enabled/disabled in Apps. Should be true for all third-party apps.
componentspage · block · modalRequired. Array of component configurations that describe how the module renders and where. See components[] array.
routepageRequired. Route path of the page (e.g., '/portfolio').
hookshookRequired. Array of hooks to register at runtime.

details object

Metadata that describes who built the module and how it should appear in Apps.

PropertyUsed withDescription
developerAll modulesRequired. Developer or organization name. For third-party apps, must not be "EW Administrator".
titleAppsRequired. Name shown in Apps and the app details UI.
descriptionAppsRequired. Short explanation of what the app does.
tagsAppsRequired. Array of tags. Currently allowed values: 'chains', 'payment', 'secretPhrase'.
routeIdAppsRequired. Used to link to the app details page.

components[] array

Each entry describes how and where a visual module is rendered.

PropertyUsed with typesDescription
componentpage · block · modalRequired. The React component to render, lazy-loaded.
initModuleEffectspage · block · modalOptional. A function to register module side effects (e.g., adding items to the sidebar) when the module is initialized.
classMappage · block · modalOptional. Class names passed down to the module component for layout variations.
destinationblockRequired for injected modules. Name of the parent module this module should be injected into.
targetSlotblockRequired for injected modules. Slot name in the parent module where this module should be rendered. Must match one of the target parent's parentSlots.
parentStyleblockOptional. Styles for the parent slot container.
orderblockRequired 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:

  • name is set and unique.

  • type is correct ('page', 'block', 'modal', 'hook', or 'config').

  • doesSupportOfflineMode is set (true/false) according to your module’s behavior.

  • paths includes all main files for your module (correct relative paths, with extensions).

  • isRemoveAble is set appropriately:

    • usually false for runtime modules,
    • can be true for pure config modules.
  • isToggleable is true for third-party apps.

  • isAvailableInStore is true for apps.

  • isDefault is set to false for third-party apps.

  • parentSlots is defined on parent modules that host blocks.

  • ✅ For blocks embedded into parent modules:

    • destination matches the parent module’s name.
    • targetSlot matches one of the parent’s parentSlots values.
    • order is set when multiple blocks share the same targetSlot.
  • details.developer is not 'EW Administrator' for third-party apps.

  • details.title, description, tags, and routeId are set for apps.

  • route is set for page modules.

  • ✅ 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

  1. Open DevTools (Ctrl/Cmd + Shift + I or right-click → Inspect).
  2. Go to the Application tab.
  3. In the sidebar, select Service Workers under the Application section.
  4. Click Unregister for all service workers.
  5. Under Cache Storage, delete all caches.
  6. 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 paths is correct and relative to module.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/modal vs. hook vs. config modules 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.