notification-system
Notification System
This skill documents the OneKey push notification implementation across all platforms.
Platform Support Matrix
| Platform | Offline Push | Notification Bar | Click Navigation | In-App Toast |
|---|---|---|---|---|
| iOS | ✅ JPush | ✅ | ✅ | ✅ |
| Android | ✅ JPush | ✅ | ✅ | ✅ |
| Desktop macOS | ❌ | ✅ | ✅ | ✅ |
| Desktop Windows | ❌ | ✅ | ❌ | ✅ |
| Desktop Linux | ❌ | ✅ | ❌ | ✅ |
| Extension | ⚠️ (browser alive) | ✅ | ✅ | ✅ |
| Web | ❌ | ✅ | ✅ | ✅ |
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Push Sources │
├─────────────────────┬───────────────────────────────────────────┤
│ JPush │ WebSocket │
│ (iOS/Android) │ (All platforms) │
└─────────────────────┴───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ServiceNotification │
│ packages/kit-bg/src/services/ServiceNotification/ │
│ - onNotificationReceived │
│ - onNotificationClicked │
│ - handleColdStartByNotification │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ notificationsUtils │
│ packages/shared/src/utils/notificationsUtils.ts │
│ - navigateToNotificationDetail │
│ - parseNotificationPayload │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
Transaction App Events Direct
Detail (EventBus) Navigation
Key Files Reference
| Purpose | Location |
|---|---|
| Notification service | packages/kit-bg/src/services/ServiceNotification/ServiceNotification.ts |
| Navigation utilities | packages/shared/src/utils/notificationsUtils.ts |
| Notification types | packages/shared/types/notification.ts |
| Cold start (native) | packages/kit/src/provider/Container/NotificationHandlerContainer/hooks.native.ts |
| Cold start (other) | packages/kit/src/provider/Container/NotificationHandlerContainer/hooks.ts |
| Event handlers | packages/kit/src/provider/Container/NotificationHandlerContainer/index.tsx |
| In-app toast | packages/kit/src/provider/Container/InAppNotification/index.tsx |
| Toast component | packages/components/src/actions/Toast/index.tsx |
| Payload test UI | packages/kit/src/views/Setting/pages/Tab/DevSettingsSection/NotificationPayloadTest.tsx |
Dev Settings: Notification Payload Test
Location: packages/kit/src/views/Setting/pages/Tab/DevSettingsSection/NotificationPayloadTest.tsx
A developer tool for testing notification payload parsing and navigation without sending actual push notifications.
Access Path
Settings → Dev Settings → Notification Payload Test
Features
- Mode Selection: Dropdown to select notification mode (1-5)
- Payload Editor: Text area to input/edit JSON or URL payload
- Load Example: Button to load default example payload for selected mode
- Test Button: Calls
parseNotificationPayloaddirectly to test navigation
Default Example Payloads
const payloadExamples = {
// Mode 1: Page Navigation - Navigate to modal
[ENotificationPushMessageMode.page]: {
screen: 'modal',
params: {
screen: 'SettingModal',
params: {
screen: 'SettingPerpUserConfig',
},
},
},
// Alternative: Navigate to main tab
// {
// screen: 'main',
// params: {
// screen: 'Discovery',
// params: {
// screen: 'TabDiscovery',
// },
// },
// },
// Mode 2: Dialog
[ENotificationPushMessageMode.dialog]: {
title: 'Test Dialog',
description: 'This is a test dialog from notification payload.',
confirmButtonProps: { text: 'Confirm' },
cancelButtonProps: { text: 'Cancel' },
onConfirm: {
actionType: 'openInBrowser',
payload: 'https://onekey.so',
},
},
// Mode 3: Open in Browser
[ENotificationPushMessageMode.openInBrowser]: 'https://onekey.so',
// Mode 4: Open in App
[ENotificationPushMessageMode.openInApp]: 'https://onekey.so/support',
// Mode 5: Open in DApp
[ENotificationPushMessageMode.openInDapp]: 'https://app.uniswap.org',
};
Usage
- Open Dev Settings in the app
- Find "Notification Payload Test" section
- Select the notification mode you want to test
- Edit the payload JSON/URL as needed
- Click "Test parseNotificationPayload" to trigger the navigation
This is useful for:
- Testing new navigation routes before backend integration
- Debugging notification payload formats
- Verifying dialog configurations
Notification Click Flow
onNotificationClicked (ServiceNotification.ts:243-288)
When a notification is clicked:
onNotificationClicked = async ({
notificationId,
params,
webEvent,
eventSource,
}: INotificationClickParams) => {
// 1. Skip if notificationId is empty (Huawei HarmonyOS edge case)
if (!notificationId) return;
// 2. Mark as shown to prevent duplicates
this.addShowedNotificationId(notificationId);
// 3. Acknowledge notification (for analytics/server sync)
void this.ackNotificationMessage({
msgId: notificationId,
action: ENotificationPushMessageAckAction.clicked,
remotePushMessageInfo: params?.remotePushMessageInfo,
});
// 4. Show and focus the app
await (await this.getNotificationProvider()).showAndFocusApp();
// 5. Wait for app to open, then navigate
await timerUtils.wait(400);
await notificationsUtils.navigateToNotificationDetail({
message: params?.remotePushMessageInfo,
isFromNotificationClick: true,
notificationId: notificationId || '',
notificationAccountId: params?.remotePushMessageInfo?.extras?.params?.accountId,
mode: params?.remotePushMessageInfo?.extras?.mode,
payload: params?.remotePushMessageInfo?.extras?.payload,
});
// 6. Remove notification from notification center
void this.removeNotification({ notificationId, desktopNotification });
};
navigateToNotificationDetail Logic
Location: packages/shared/src/utils/notificationsUtils.ts:175-315
Function Signature
async function navigateToNotificationDetail({
notificationId,
notificationAccountId,
message,
isFromNotificationClick,
navigation,
mode,
payload,
topicType,
isRead = false,
}: INavigateToNotificationDetailParams)
Navigation Decision Tree
┌─────────────────┐
│ Has mode set? │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ Yes │ No
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│parseNotification│ │ Has transactionHash │
│ Payload │ │ in extras? │
└─────────────────┘ └──────────┬──────────┘
│
┌──────────┴──────────┐
│ Yes │ No
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Navigate to │ │ Navigate to │
│HistoryDetails │ │ NotificationList│
│ modal │ │ (default) │
└─────────────────┘ └─────────────────┘
Key Logic
- Log analytics (if not already read)
- Check current route - If already in NotificationsModal, just update
- Transaction notifications: Navigate to
HistoryDetailsmodal with transaction params - Mode-based navigation: Call
parseNotificationPayloadif mode is set - Default behavior: Navigate to
NotificationList
parseNotificationPayload Logic
Location: packages/shared/src/utils/notificationsUtils.ts:127-173
Notification Modes
export enum ENotificationPushMessageMode {
page = 1, // Navigate to a specific page
dialog = 2, // Show a dialog
openInBrowser = 3, // Open URL in external browser
openInApp = 4, // Open URL in in-app browser
openInDapp = 5, // Open URL in DApp browser
}
Mode Handlers
| Mode | Event/Action | Handler Location |
|---|---|---|
page (1) |
EAppEventBusNames.ShowNotificationPageNavigation |
NotificationHandlerContainer/index.tsx:104-121 |
dialog (2) |
EAppEventBusNames.ShowNotificationViewDialog |
NotificationHandlerContainer/index.tsx:64-99 |
openInBrowser (3) |
openUrlExternal(payload) |
Direct call |
openInApp (4) |
openUrlInApp(payload) |
Direct call |
openInDapp (5) |
EAppEventBusNames.ShowNotificationInDappPage |
NotificationHandlerContainer/index.tsx:122-140 |
Implementation
export function parseNotificationPayload(
mode: ENotificationPushMessageMode,
payload: string | undefined,
fallbackHandler: () => void,
) {
switch (mode) {
case ENotificationPushMessageMode.page:
// Parse JSON payload and emit navigation event
const payloadObj = JSON.parse(payload || '');
appEventBus.emit(EAppEventBusNames.ShowNotificationPageNavigation, {
payload: payloadObj,
});
break;
case ENotificationPushMessageMode.dialog:
// Parse JSON payload and emit dialog event
const payloadObj = JSON.parse(payload || '');
appEventBus.emit(EAppEventBusNames.ShowNotificationViewDialog, {
payload: payloadObj,
});
break;
case ENotificationPushMessageMode.openInBrowser:
openUrlExternal(payload);
break;
case ENotificationPushMessageMode.openInApp:
openUrlInApp(payload);
break;
case ENotificationPushMessageMode.openInDapp:
appEventBus.emit(EAppEventBusNames.ShowNotificationInDappPage, payload);
break;
}
}
Backend Configuration Guide
Notification Message Structure
interface INotificationPushMessageExtras {
msgId: string;
miniBundlerVersion?: string; // Minimum app version required
mode?: ENotificationPushMessageMode; // 1-5
payload?: string; // JSON string or URL
topic: ENotificationPushTopicTypes;
image?: string; // Image URL for notification
params: {
msgId: string;
accountAddress: string;
accountId: string;
networkId: string;
transactionHash: string;
};
}
Mode Configuration Examples
Mode 1: Page Navigation
{
"mode": 1,
"payload": "{\"screen\":\"Modal\",\"params\":{\"screen\":\"SettingsModal\",\"params\":{\"screen\":\"SettingsPage\"}}}"
}
Payload supports local param replacement:
{
"screen": "Modal",
"params": {
"screen": "EarnModal",
"params": {
"screen": "EarnDetail",
"params": {
"networkId": "evm--1",
"accountId": "{local_accountId}"
}
}
}
}
Available local params:
{local_accountId}- Current account ID{local_indexedAccountId}- Current indexed account ID{local_networkId}- Current network ID{local_walletId}- Current wallet ID
Generate All Available Routes
Run the following command to generate a complete list of all navigable routes with ready-to-use Mode 1 JSON payloads:
npx tsx development/scripts/extract-routes.ts
This will generate:
build/routes/ROUTES.md- Markdown documentation with all routes and their parametersbuild/routes/routes.json- JSON format for programmatic access
Each route entry includes:
- Required and optional parameters
- Pre-filled
{local_*}template variables for common params - Complete Mode 1 JSON payload ready to copy
Mode 2: Dialog
{
"mode": 2,
"payload": "{\"title\":\"Update Available\",\"description\":\"A new version is available.\",\"onConfirm\":{\"actionType\":\"openInBrowser\",\"payload\":\"https://onekey.so\"}}"
}
Dialog payload structure:
interface INotificationViewDialogPayload {
title?: string;
description?: string;
icon?: IKeyOfIcons;
tone?: 'default' | 'destructive';
confirmButtonProps?: { text: string };
cancelButtonProps?: { text: string };
onConfirm: {
actionType: 'navigate' | 'openInApp' | 'openInBrowser';
payload: string | NavigationPayload;
};
}
Mode 3: Open in External Browser
{
"mode": 3,
"payload": "https://onekey.so/blog/announcement"
}
Mode 4: Open in In-App Browser
{
"mode": 4,
"payload": "https://onekey.so/support"
}
Mode 5: Open in DApp Browser
{
"mode": 5,
"payload": "https://app.uniswap.org"
}
Cold Start Notification Handling
Native Platforms (iOS/Android)
Location: packages/kit/src/provider/Container/NotificationHandlerContainer/hooks.native.ts
The useInitialNotification hook handles app cold start via notification:
export const useInitialNotification = () => {
const coldStartRef = useRef(true);
useEffect(() => {
setTimeout(async () => {
if (coldStartRef.current) {
coldStartRef.current = false;
// 1. Check ColdStartByNotification (JPush)
const options = ColdStartByNotification.launchNotification;
if (options) {
// Handle JPush launch notification
void backgroundApiProxy.serviceNotification.handleColdStartByNotification({
notificationId: options.msgId,
params: { /* notification details */ },
});
return;
}
// 2. Check LaunchOptionsManager (local/remote notifications)
const launchOptions = await launchOptionsManager.getLaunchOptions();
if (launchOptions?.localNotification || launchOptions?.remoteNotification) {
const userInfo = launchOptions.localNotification?.userInfo
|| launchOptions.remoteNotification?.userInfo;
await handleShowNotificationDetail({
message: userInfo,
notificationId: userInfo?.extras?.params?.msgId,
mode: userInfo?.extras?.mode,
payload: userInfo?.extras?.payload,
});
}
}
}, 350);
}, []);
};
Non-Native Platforms
Location: packages/kit/src/provider/Container/NotificationHandlerContainer/hooks.ts
export const useInitialNotification = () => {}; // No-op
Cold start handling is not needed for web/desktop/extension as:
- Web: Notifications don't persist across page reloads
- Desktop: App opens fresh, WebSocket reconnects
- Extension: Background script maintains state
In-App Notification Toast
Location: packages/kit/src/provider/Container/InAppNotification/index.tsx:410-461
When It Triggers
In-app notifications show when:
- WebSocket notification is received (
pushSource === 'websocket') - Platform is NOT iOS native (iOS uses native notification center)
Implementation
useEffect(() => {
const callback = ({
notificationId,
title,
description,
icon,
remotePushMessageInfo,
}) => {
const topicType = remotePushMessageInfo?.extras?.topic;
const isSystemTopic = topicType === ENotificationPushTopicTypes.system;
const toast = Toast.notification({
title,
message: description,
icon: isSystemTopic ? undefined : (icon as IKeyOfIcons),
iconImageUri: isSystemTopic ? undefined : remotePushMessageInfo?.extras?.image,
duration: 10 * 1000,
imageUri: remotePushMessageInfo?.extras?.image,
onPress: async () => {
await whenAppUnlocked();
await notificationsUtils.navigateToNotificationDetail({
message: remotePushMessageInfo,
isFromNotificationClick: true,
notificationId: notificationId || '',
mode: remotePushMessageInfo?.extras?.mode,
payload: remotePushMessageInfo?.extras?.payload,
});
toast.close();
},
});
};
appEventBus.on(EAppEventBusNames.ShowInAppPushNotification, callback);
return () => {
appEventBus.off(EAppEventBusNames.ShowInAppPushNotification, callback);
};
}, [navigation]);
Toast.notification Props
interface IToastNotificationProps {
title: string;
message?: string;
icon?: IKeyOfIcons;
iconImageUri?: string; // Custom icon image
imageUri?: string; // Large image on right
duration?: number; // Default: 5000ms
onPress?: () => void; // Click handler
onClose?: () => void; // Close callback
}
Customizing Toast Appearance
The Toast.notification component is defined in:
packages/components/src/actions/Toast/index.tsx:272-375
Key styling elements:
- Icon container:
bg="$bgStrong",borderRadius="$full", 28x28px - Title:
size="$headingSm", max 2 lines - Message:
size="$bodyMd",color="$textSubdued", max 3 lines - Image:
borderRadius="$1",size="$12"(48px)
Other parseNotificationPayload Usages
1. Hardware Device Get Started (DeviceGetStarted.tsx)
Location: packages/kit/src/views/DeviceManagement/pages/DeviceDetailsModal/DeviceGetStarted.tsx:38-39
const handleOpen = (item: { mode: number; payload: string }) => {
parseNotificationPayload(item.mode, item.payload, () => {});
};
Used for hardware wallet tutorial and FAQ links fetched from server.
2. Wallet Banner Clicks (useWalletBanner.ts)
Location: packages/kit/src/hooks/useWalletBanner.ts:69-72
if (item.mode) {
parseNotificationPayload(item.mode, item.payload, () => {});
return;
}
Used for promotional banners in the wallet home screen.
Event Bus Events
| Event Name | Trigger | Handler |
|---|---|---|
ShowInAppPushNotification |
WebSocket notification received | InAppNotification/index.tsx |
ShowNotificationPageNavigation |
Mode 1 payload parsed | NotificationHandlerContainer/index.tsx |
ShowNotificationViewDialog |
Mode 2 payload parsed | NotificationHandlerContainer/index.tsx |
ShowNotificationInDappPage |
Mode 5 payload parsed | NotificationHandlerContainer/index.tsx |
ShowFallbackUpdateDialog |
Version mismatch | NotificationHandlerContainer/index.tsx |
UpdateNotificationBadge |
Badge count change | Various UI components |