Helps create and configure pages and routes in the OneKey app-monorepo. Use when creating new pages, configuring routes, setting up deep links, handling QR codes, or understanding navigation patterns...
This skill helps create and configure pages and routes in the OneKey app-monorepo.
DO NOT DELETE PAGES unless you have confirmed there are NO external links to the page.
External links include:
If you need to remove a page's functionality:
Instead of deleting the route, keep the route registered and modify the page component:
import { Page } from '@onekeyhq/components';
/**
* @deprecated This page has been deprecated since v5.0.0
*
* New location: packages/kit/src/views/NewFeature/pages/NewPage.tsx
* New route: EModalRoutes.NewFeatureModal -> ENewFeatureRoutes.NewPage
*
* This page is kept for backward compatibility with external deep links.
* DO NOT DELETE - external links may still reference this route.
*/
function DeprecatedPage() {
return (
<Page
shouldRedirect={() => true} // Always redirect
onRedirected={() => {
// Navigate to replacement page or home
navigation.switchTab(ETabRoutes.Home);
// Or show a toast explaining the change
Toast.info({ title: 'This feature has been moved' });
}}
>
{null} // Render nothing
</Page>
);
}
export default DeprecatedPage;
This approach ensures:
The page types are defined in packages/components/src/hocs/PageType/pageType.ts:
| Type | Description | Animation Behavior |
|---|---|---|
modal |
Modal route pages | iOS: First page slides from bottom, subsequent pages slide from right. Android: No animation. Web/Desktop/Extension/iPad: Same as iOS but displays as floating popup |
stack |
Tab route pages (will be renamed from "stack") | On small screens, first page shows in bottom tab bar; on large screens, tab shows in sidebar. Subsequent pages slide from right. Android: No animation |
fullScreen |
Full screen overlay (deprecated) | Layer is lower than modal, avoid adding new pages |
onboarding |
OnboardingV2 full screen pages | Full screen overlay on all platforms, layer is below modal |
Modal routes are configured in packages/kit/src/routes/Modal/router.tsx.
Example: Creating a NotificationsModal route
Create route file packages/shared/src/routes/notifications.ts:
export enum EModalNotificationsRoutes {
NotificationList = 'NotificationList',
NotificationIntroduction = 'NotificationIntroduction',
}
export type IModalNotificationsParamList = {
[EModalNotificationsRoutes.NotificationList]: undefined;
[EModalNotificationsRoutes.NotificationIntroduction]: undefined;
};
In packages/shared/src/routes/modal.ts:
export enum EModalRoutes {
// ... existing routes
NotificationsModal = 'NotificationsModal',
}
export type IModalParamList = {
// ... existing types
[EModalRoutes.NotificationsModal]: IModalNotificationsParamList;
};
Create packages/kit/src/views/Notifications/router/index.ts:
import type { IModalFlowNavigatorConfig } from '@onekeyhq/components';
import { LazyLoadPage } from '@onekeyhq/kit/src/components/LazyLoadPage';
import type { IModalNotificationsParamList } from '@onekeyhq/shared/src/routes/notifications';
import { EModalNotificationsRoutes } from '@onekeyhq/shared/src/routes/notifications';
const NotificationList = LazyLoadPage(
() => import('@onekeyhq/kit/src/views/Notifications/pages/NotificationList'),
);
const NotificationIntroduction = LazyLoadPage(
() => import('@onekeyhq/kit/src/views/Notifications/pages/NotificationIntroduction'),
);
export const ModalNotificationsRouter: IModalFlowNavigatorConfig<
EModalNotificationsRoutes,
IModalNotificationsParamList
>[] = [
{
name: EModalNotificationsRoutes.NotificationList,
component: NotificationList,
},
{
name: EModalNotificationsRoutes.NotificationIntroduction,
component: NotificationIntroduction,
},
];
In packages/kit/src/routes/Modal/router.tsx:
import { ModalNotificationsRouter } from '../../views/Notifications/router';
const router: IModalRootNavigatorConfig<EModalRoutes>[] = [
// ... existing routes
{
name: EModalRoutes.NotificationsModal,
children: ModalNotificationsRouter,
},
];
Create packages/kit/src/views/Notifications/pages/NotificationList.tsx:
import { Page } from '@onekeyhq/components';
function NotificationList() {
return (
<Page safeAreaEnabled={false}>
<Page.Body>
{/* Page content */}
</Page.Body>
</Page>
);
}
export default NotificationList;
Onboarding routes are configured in packages/kit/src/views/Onboardingv2/router/index.tsx.
Example: Creating AddExistingWallet page
In packages/shared/src/routes/onboarding.ts:
export enum EOnboardingPagesV2 {
GetStarted = 'GetStarted',
AddExistingWallet = 'AddExistingWallet',
// ... more pages
}
In packages/kit/src/views/Onboardingv2/router/index.tsx:
const AddExistingWallet = LazyLoadPage(
() => import('../pages/AddExistingWallet'),
undefined,
false,
<OnboardingLayoutFallback />,
);
export const OnboardingRouterV2: IModalFlowNavigatorConfig<
EOnboardingPagesV2,
IOnboardingParamListV2
>[] = [
// ... other routes
{
name: EOnboardingPagesV2.AddExistingWallet,
component: AddExistingWallet,
options: hiddenHeaderOptions,
},
];
Tab routes are configured in packages/kit/src/routes/Tab/router.ts.
Example: Creating Market tab with sub-pages
In packages/shared/src/routes/tabMarket.ts:
export enum ETabMarketRoutes {
TabMarket = 'TabMarket',
MarketDetail = 'MarketDetail',
MarketDetailV2 = 'MarketDetailV2',
MarketNativeDetail = 'MarketNativeDetail',
MarketBannerDetail = 'MarketBannerDetail',
}
export type ITabMarketParamList = {
[ETabMarketRoutes.TabMarket]: { from?: EEnterWay } | undefined;
[ETabMarketRoutes.MarketDetail]: { token: string };
[ETabMarketRoutes.MarketDetailV2]: {
tokenAddress: string;
network: string;
isNative?: boolean;
};
// ... more params
};
Create packages/kit/src/routes/Tab/Marktet/router.ts:
import type { ITabSubNavigatorConfig } from '@onekeyhq/components';
import { ETabMarketRoutes } from '@onekeyhq/shared/src/routes';
import { LazyLoadRootTabPage, LazyLoadPage } from '../../../components/LazyLoadPage';
const MarketHome = LazyLoadRootTabPage(() => import('../../../views/Market/MarketHome'));
const MarketDetail = LazyLoadPage(() => import('../../../views/Market/MarketDetail'));
export const marketRouters: ITabSubNavigatorConfig<any, any>[] = [
{
rewrite: '/',
name: ETabMarketRoutes.TabMarket,
headerShown: !platformEnv.isNative,
component: MarketHome,
},
{
name: ETabMarketRoutes.MarketDetail,
component: MarketDetail,
rewrite: '/tokens/:token',
},
];
In packages/kit/src/routes/Tab/router.ts:
import { marketRouters } from './Marktet/router';
export const useTabRouterConfig = () => {
return useMemo(() => {
const tabs = [
{
name: ETabRoutes.Market,
tabBarIcon: (focused?: boolean) =>
focused ? 'ChartTrendingUp2Solid' : 'ChartTrendingUp2Outline',
translationId: ETranslations.global_market,
rewrite: '/market',
exact: true,
children: marketRouters,
// Control visibility per platform
hiddenIcon: platformEnv.isNative, // Hide on mobile
},
// ... other tabs
];
return tabs;
}, []);
};
Tab Configuration Options:
hiddenIcon: Hide tab icon on certain platformshideOnTabBar: Hide from tab bar but keep route accessibleinMoreAction: Show in "More" menu instead of main tab barfreezeOnBlur: Keep tab state when switching tabsRoute paths MUST NOT be duplicated. Each route path must be unique across the entire application.
If duplicate paths are detected, the application will throw an error at startup:
Found conflicting screens with the same pattern. The pattern '/market/tokens/.'
resolves to both 'Main > Market > MarketDetail' and 'Main > Market > MarketDetailV2'.
Patterns must be unique and cannot resolve to more than one screen.
Common causes of duplicate paths:
rewrite valueHow to avoid:
rewrite values for each route/token/:network vs /token/:network/:tokenAddress)Route paths are calculated in packages/kit/src/routes/config/index.ts via resolveScreens.
interface IScreenRouterConfig {
name: string; // Route name (used as default path segment)
rewrite?: string; // Override path segment
exact?: boolean; // If true, ignores parent paths and uses only rewrite
children?: IScreenRouterConfig[];
}
// resolveScreens transforms route config into path config
const resolveScreens = (routes: IScreenRouterConfig[]) =>
routes.reduce((prev, route) => {
prev[route.name] = {
path: route.rewrite ? route.rewrite : route.name, // Use rewrite or name as path
exact: !!route.exact,
};
if (route.children) {
prev[route.name].screens = resolveScreens(route.children); // Recursive for children
}
return prev;
}, {});
How it works:
name becomes its key in the configpath is either rewrite value (if provided) or nameexact flag determines if parent paths are includedRule 1: Default path accumulation Without any configuration, paths accumulate based on route hierarchy:
Route: Main -> Market -> MarketDetail
Path: /Main/Market/MarketDetail
Rule 2: rewrite replaces current segment only
// Tab router config
{
name: ETabRoutes.Market,
rewrite: '/market', // Replaces 'Market' with 'market'
children: marketRouters,
}
// Result: /Main/market/... (only current segment changed)
Rule 3: exact: true truncates all parent paths
{
name: EModalRoutes.SettingModal,
children: ModalSettingStack,
rewrite: '/settings',
exact: true, // Ignores /Modal prefix
}
// Without exact: /Modal/settings/...
// With exact: /settings/...
Rule 4: Path params with /:param syntax
// In marketRouters
{
name: ETabMarketRoutes.MarketDetail,
component: MarketDetail,
rewrite: '/tokens/:token', // :token is a path parameter
}
// Result path pattern: /market/tokens/:token
// Actual URL example: /market/tokens/bitcoin
Example 1: Tab Route with rewrite + exact
// Tab router (packages/kit/src/routes/Tab/router.ts)
{
name: ETabRoutes.Market,
rewrite: '/market',
exact: true, // Path starts from /market, not /Main/Market
children: marketRouters,
}
// Sub-router (packages/kit/src/routes/Tab/Marktet/router.ts)
export const marketRouters = [
{
name: ETabMarketRoutes.TabMarket,
rewrite: '/', // Root of /market
component: MarketHome,
},
{
name: ETabMarketRoutes.MarketDetail,
rewrite: '/tokens/:token',
component: MarketDetail,
},
];
// Resulting paths:
// TabMarket: /market/
// MarketDetail: /market/tokens/:token (e.g., /market/tokens/bitcoin)
Example 2: Modal Route with exact
// Modal router (packages/kit/src/routes/Modal/router.tsx)
{
name: EModalRoutes.SettingModal,
children: ModalSettingStack,
rewrite: '/settings',
exact: true,
}
// Resulting path: /settings/... (not /Modal/settings/...)
Example 3: Onboarding Route
// Onboarding router config
{
name: EOnboardingV2Routes.OnboardingV2,
rewrite: '/onboarding',
exact: true,
children: OnboardingRouterV2,
}
// Sub-routes
{
name: EOnboardingPagesV2.GetStarted,
rewrite: '/get-started',
component: GetStarted,
}
// Resulting path: /onboarding/get-started
Example 4: Native token detail (no tokenAddress in path)
{
name: ETabMarketRoutes.MarketNativeDetail,
component: MarketDetailV2,
rewrite: '/token/:network', // Only network, no token address
}
// URL example: /market/token/eth
Example 5: Non-native token detail (with tokenAddress)
{
name: ETabMarketRoutes.MarketDetailV2,
component: MarketDetailV2,
rewrite: '/token/:network/:tokenAddress',
}
// URL example: /market/token/eth/0x1234...
The pagePath template function in routeUtils.ts calculates full paths:
function pagePath(_: TemplateStringsArray, ...screenNames: string[]): string {
const path = screenNames.reduce((prev, screenName) => {
const screen = screenConfig[screenName];
const paths = screen.path.split('/:');
const rawPath = removeExtraSlash(paths[0]);
const screenPath = paths.length > 1 ? `${rawPath}/.` : rawPath;
// If exact, use only current path; otherwise, accumulate
return screen.exact ? screenPath : addPath(prev, screenPath);
}, '');
return `/${path}`;
}
// Usage:
pagePath`${ERootRoutes.Main}${ETabRoutes.Market}${ETabMarketRoutes.MarketDetail}`
// Result: /market/tokens/. (the /. indicates path params follow)
By default, routes don't display in the browser URL bar. To enable URL display, configure in packages/shared/src/utils/routeUtils.ts:
export const buildAllowList = (screens, perpDisabled, perpTabShowWeb) => {
const rules = {
// Show URL with parameters
[pagePath`${ERootRoutes.Main}${ETabRoutes.Market}${ETabMarketRoutes.MarketDetailV2}`]: {
showUrl: true, // Enable URL display
showParams: true, // Show query parameters
},
// Show URL without parameters
[pagePath`${ERootRoutes.Main}${ETabRoutes.Swap}${ETabSwapRoutes.TabSwap}`]: {
showUrl: true,
showParams: false, // Hide query parameters
},
};
return rules;
};
Note: When adding new pages that need browser URL display, configure them in buildAllowList.
The routing implementation differs between web/native platforms and browser extension:
File: packages/kit/src/routes/config/getStateFromPath.ts
// Simply re-exports from React Navigation
export { getStateFromPath } from '@react-navigation/core';
URL format: https://app.onekey.so/market/tokens/bitcoin
File: packages/kit/src/routes/config/getStateFromPath.ext.ts
The extension uses a custom implementation copied from @react-navigation/core with hash routing modifications. The file is marked with // ---CHANGED Begin---- and // ---CHANGED end---- comments to indicate customizations.
Why custom implementation is needed:
ui-expand-tab.html)Key customizations in getStateFromPath.ext.ts:
// 1. Store initial pathname for later use (line 98-100)
// ---CHANGED Begin----: initial path from url
let initialPath: string | undefined = globalThis.location.pathname;
// ---CHANGED end----
// 2. Extract path from hash instead of pathname (line 107-109)
export function getStateFromPath<ParamList extends {}>(
path: string,
options?: Options<ParamList>
): ResultState | undefined {
// ---CHANGED Begin----: support hash router
path = globalThis.location.hash.split('#').pop() || '/';
// ---CHANGED end----
// ... rest of function
}
// 3. Rewrite initial path for focused route (line 731-736)
// In createNestedStateObject function:
route = findFocusedRoute(state) as ParsedRoute;
// ---CHANGED Begin----: rewrite hash path to initial path
if (initialPath) {
route.path = initialPath;
initialPath = undefined;
}
// ---CHANGED end----
URL format returned (in index.ts line 139):
return `${extHtmlFileUrl}#${newPath}`;
// Example: /ui-expand-tab.html#/market/tokens/bitcoin
URL format comparison:
| Platform | URL Format |
|---|---|
| Web | https://app.onekey.so/market/tokens/bitcoin |
| Extension | chrome-extension://xxx/ui-expand-tab.html#/market/tokens/bitcoin |
Important notes for extension routing:
# symbol separates the HTML file from the route path// /ui-expand-tab.html/#/ NOT working for Windows Chrome
// /ui-expand-tab.html#/ works fine
The getStateFromPath.ext.ts file contains 4 key modifications from the original @react-navigation/core implementation:
| # | Location | Modification | Functional Impact |
|---|---|---|---|
| 1 | Lines 33-37 | Import changes | Uses OneKeyLocalError instead of native Error; imports utilities from @react-navigation/core |
| 2 | Lines 98-100 | let initialPath = globalThis.location.pathname |
Stores HTML file path (e.g., /ui-expand-tab.html) for later use in route state |
| 3 | Lines 107-109 | path = globalThis.location.hash.split('#').pop() |
Core change: Extracts route path from URL hash instead of using passed parameter |
| 4 | Lines 731-736 | Rewrite route.path with initialPath |
Sets focused route's path to initial HTML file path for proper state management |
Why these modifications are necessary:
/market/tokens/btc) doesn't work with chrome-extension:// URLs#/market/tokens/btc) allows client-side navigation without server requestsIn packages/kit/src/routes/config/index.ts, the getPathFromState function handles platform differences:
getPathFromState(state, options) {
// ... calculate path ...
if (platformEnv.isExtension) {
// Extension: return hash-based URL
if (newPath === '/' && globalThis.location.href.endsWith('#/')) {
return extHtmlFileUrl; // e.g., /ui-expand-tab.html
}
return `${extHtmlFileUrl}#${newPath}`; // e.g., /ui-expand-tab.html#/market
}
// Web/Desktop: return standard path
return newPath; // e.g., /market
}
The Page component supports lifecycle callbacks defined in packages/components/src/layouts/Page/type.ts:
interface IPageLifeCycle {
onMounted?: () => void; // Called after page transition completes
onUnmounted?: () => void; // Called after page unmount transition completes
onCancel?: () => void; // Called when page closed without confirm
onConfirm?: () => void; // Called when page closed with confirm
onClose?: (extra?: { flag?: string }) => void; // Called on any close
onRedirected?: () => void; // Called after redirect completes
shouldRedirect?: () => boolean; // Return true to redirect immediately
}
Usage in Page Component:
import { Page } from '@onekeyhq/components';
function MyPage() {
return (
<Page
onMounted={() => console.log('Page mounted')}
onUnmounted={() => console.log('Page unmounted')}
shouldRedirect={() => !hasPermission}
onRedirected={() => navigation.navigate('Login')}
>
<Page.Body>{/* content */}</Page.Body>
</Page>
);
}
Use Cases:
| Callback | Use Case |
|---|---|
onMounted |
Load data, start animations, track page views |
onUnmounted |
Cleanup resources, cancel requests |
onClose |
Save draft, confirm unsaved changes |
onCancel |
Track cancel actions, cleanup form state |
onConfirm |
Track confirm actions, submit data |
shouldRedirect |
Auth guards, permission checks |
onRedirected |
Navigate to login, show toast |
Redirect Pattern:
<Page
shouldRedirect={() => {
// Return true to immediately go back and trigger onRedirected
return !isUserAuthenticated;
}}
onRedirected={() => {
// Called after going back, navigate to appropriate screen
navigation.pushModal(EModalRoutes.OnboardingModal, {
screen: EOnboardingPages.Login,
});
}}
>
Configure in packages/kit/src/routes/config/deeplink/index.ts:
Example: Adding invite_share deep link
In packages/shared/src/consts/deeplinkConsts.tsx:
export enum EOneKeyDeepLinkPath {
url_account = 'url_account',
market_detail = 'market_detail',
invite_share = 'invite_share', // Add new path
}
export type IEOneKeyDeepLinkParams = {
[EOneKeyDeepLinkPath.invite_share]: {
utm_source: string;
code: string;
};
};
In packages/kit/src/routes/config/deeplink/index.ts:
async function processDeepLinkUrlAccount(params, times = 0) {
const { parsedUrl } = params;
const { hostname, queryParams, scheme, path } = parsedUrl;
if (scheme === ONEKEY_APP_DEEP_LINK || scheme === ONEKEY_APP_DEEP_LINK_NAME) {
switch (hostname ?? path?.slice(1)) {
case EOneKeyDeepLinkPath.invite_share: {
const { utm_source: utmSource, code } = queryParams as IEOneKeyDeepLinkParams[EOneKeyDeepLinkPath.invite_share];
if (navigation) {
navigation.switchTab(ETabRoutes.ReferFriends, {
screen: ETabReferFriendsRoutes.TabReferAFriend,
params: { utmSource, code },
});
}
break;
}
}
}
}
Deep Link Format:
onekey-wallet://invite_share?utm_source=twitter&code=ABC123https://app.onekey.so/wc/connect/wc?uri=... (Universal Link)Configure QR code handlers for scanning and banner clicks.
Example: MARKET_DETAIL handler
In packages/shared/types/qrCode.ts:
export enum EQRCodeHandlerType {
MARKET_DETAIL = 'MARKET_DETAIL',
// ... other types
}
export enum EQRCodeHandlerNames {
marketDetail = 'marketDetail',
// ... other names
}
export const PARSE_HANDLER_NAMES = {
all: [
EQRCodeHandlerNames.marketDetail,
// ... other handlers
],
};
Create packages/kit-bg/src/services/ServiceScanQRCode/utils/parseQRCode/handlers/marketDetail.ts:
import { WEB_APP_URL, WEB_APP_URL_DEV, WEB_APP_URL_SHORT } from '@onekeyhq/shared/src/config/appConfig';
import { EQRCodeHandlerType } from '@onekeyhq/shared/types/qrCode';
import type { IMarketDetailValue, IQRCodeHandler } from '../type';
const marketDetail: IQRCodeHandler<IMarketDetailValue> = async (value, options) => {
const urlValue = options?.urlResult;
if (urlValue?.data?.urlParamList) {
const origin = urlValue?.data?.origin;
// Check if URL matches OneKey web app
if (
[WEB_APP_URL, WEB_APP_URL_DEV, WEB_APP_URL_SHORT].includes(origin) &&
urlValue?.data?.pathname.startsWith('/market/tokens/')
) {
const coinGeckoId = urlValue?.data?.pathname.split('/market/tokens/').pop();
return {
type: EQRCodeHandlerType.MARKET_DETAIL,
data: { origin, coinGeckoId },
};
}
}
return null;
};
export default marketDetail;
In packages/kit-bg/src/services/ServiceScanQRCode/utils/parseQRCode/handlers/index.ts:
import marketDetail from './marketDetail';
export const PARSE_HANDLERS = {
[EQRCodeHandlerNames.marketDetail]: marketDetail,
// ... other handlers
};
In packages/kit/src/views/ScanQrCode/hooks/useParseQRCode.tsx:
const parse = useCallback(async (value, params) => {
const result = await backgroundApiProxy.serviceScanQRCode.parse(value, options);
switch (result.type) {
case EQRCodeHandlerType.MARKET_DETAIL: {
const { coinGeckoId } = result.data as IMarketDetailValue;
if (coinGeckoId) {
await closeScanPage();
void marketNavigation.pushDetailPageFromDeeplink(navigation, { coinGeckoId });
}
break;
}
}
}, [navigation]);
// Push modal
navigation.pushModal(EModalRoutes.NotificationsModal, {
screen: EModalNotificationsRoutes.NotificationList,
params: { /* page params */ },
});
// Switch tab
navigation.switchTab(ETabRoutes.Market);
// Navigate within tab
navigation.navigate(ERootRoutes.Main, {
screen: ETabRoutes.Market,
params: {
screen: ETabMarketRoutes.MarketDetail,
params: { token: 'bitcoin' },
},
}, {
pop: true,
});
// Go back
navigation.goBack();
pop: true with navigation.navigateWhen using navigation.navigate, ALWAYS include the pop: true option to ensure proper navigation stack management:
// ✅ Correct - with pop: true
navigation.navigate(ERootRoutes.Main, undefined, {
pop: true,
});
navigation.navigate(ERootRoutes.Main, {
screen: ETabRoutes.Market,
params: {
screen: ETabMarketRoutes.MarketDetail,
params: { token: 'bitcoin' },
},
}, {
pop: true,
});
// ❌ Wrong - missing pop: true
navigation.navigate(ERootRoutes.Main, {
screen: ETabRoutes.Market,
});
Why pop: true is required:
| Purpose | Location |
|---|---|
| Page type enum | packages/components/src/hocs/PageType/pageType.ts |
| Modal routes | packages/kit/src/routes/Modal/router.tsx |
| Tab routes | packages/kit/src/routes/Tab/router.ts |
| Onboarding routes | packages/kit/src/views/Onboardingv2/router/index.tsx |
| Route enums | packages/shared/src/routes/ |
| URL allow list | packages/shared/src/utils/routeUtils.ts |
| Route config (linking) | packages/kit/src/routes/config/index.ts |
| Web/Native path parser | packages/kit/src/routes/config/getStateFromPath.ts |
| Extension path parser | packages/kit/src/routes/config/getStateFromPath.ext.ts |
| Deep link config | packages/kit/src/routes/config/deeplink/index.ts |
| Deep link consts | packages/shared/src/consts/deeplinkConsts.tsx |
| QR handlers | packages/kit-bg/src/services/ServiceScanQRCode/utils/parseQRCode/handlers/ |
| Page component | packages/components/src/layouts/Page/index.tsx |
| Page lifecycle | packages/components/src/layouts/Page/hooks.ts |