One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Notifications

Local notifications, action buttons, Android channels, and push tokens on iOS and Android

One.Notifications shows notifications from your app, schedules them for later, adds action buttons, and gets the device’s push token. On iOS it is the UserNotifications framework; on Android it is NotificationManager for posting and AlarmManager for scheduling, with Firebase Cloud Messaging for push tokens.

It covers the whole local path: permission, scheduling, how a notification shows while the app is open, and the tap that brings people back. Remote push delivery itself comes from your server through APNs or FCM; One hands you the token to send it to.

Usage

Turn notifications on in your app config, then run prebuild:

import { one } from 'one/vite'
export default {
plugins: [one({ native: { app: { name: 'Tea', notifications: {} } } })],
}

Then ask for permission and schedule:

import { One } from 'one'
async function remindInFiveMinutes() {
const { granted } = await One.Notifications.requestPermissions()
if (!granted) return
await One.Notifications.schedule({
identifier: 'tea',
content: { title: 'Tea', body: 'Your tea is ready' },
trigger: { type: 'timeInterval', seconds: 300 },
})
}

The identifier names the request, so scheduling tea again replaces it rather than adding a second reminder.

How it works

Config. notifications: {} makes prebuild mark the app as using One’s notifications: on iOS it adds an Info.plist flag that installs One’s notification delegate at launch; on Android it adds the notification permission and the receiver that delivers alarms and taps. Without it, One installs nothing, so an app that uses another push library keeps that library’s delegate.

Permission. requestPermissions shows the system prompt on iOS and, on Android 13 and later, asks for the notification permission. Older Android versions have no runtime prompt, so it resolves the current status.

Scheduling. A null trigger posts at once. timeInterval waits a number of seconds and can repeat; date fires at a time, and a past date posts at once. iOS hands the request to the system. Android stores it, sets an inexact alarm with setAndAllowWhileIdle, and posts when the alarm fires; the schedule survives restarts, and after a reboot future alarms are set again, repeats start over from boot, and one-time reminders that expired while the device was off are dropped.

While the app is open. When a notification arrives in the foreground, native code asks JavaScript how to show it. setHandler answers with a banner, list, sound, and badge choice. With no handler set, it shows in full; with the handler set to null, it stays hidden. If the handler throws or takes more than 3 seconds, native shows it in full rather than dropping it. If nothing in JavaScript listens yet, it shows at once. Every foreground arrival also fires addReceivedListener.

Taps. A tap fires addResponseReceivedListener with the notification and the action identifier: the action’s own identifier for a button, or DEFAULT_ACTION_IDENTIFIER for the body. A tap that launches the app lands before JavaScript is running, so native keeps it, and getLastResponse returns it synchronously on start. On iOS, One’s delegate installs as the app finishes launching, before that tap can arrive.

Push tokens. With notifications: { push: true }, iOS gets the APNs entitlement and calls registerForRemoteNotifications, and the token comes back as lowercase hex. Android compiles in Firebase Messaging and returns the FCM registration token. Without push, Firebase stays out of the Android app and getDevicePushToken rejects on both platforms.

API reference

Permission and badge

MethodResolves
getPermissions()The current permission, without a prompt
requestPermissions(request?)The permission after the prompt
getBadgeCount()The app icon badge
setBadgeCount(count)Whether it was set

Scheduling

MethodResolves
schedule(request)The request’s identifier
cancelScheduled(id)Once that request is removed
cancelAllScheduled()Once every pending request is removed
getAllScheduled()The pending requests
getPresented()Notifications on screen now
dismiss(id), dismissAll()Once removed from the screen

Events

MethodReturns
setHandlerNothing
addReceivedListenerA subscription
addResponseReceivedListenerA subscription
getLastResponseThe launching tap, or null
clearLastResponseNothing

Categories, channels, and push

MethodResolves
setCategory(id, actions, options?)The registered category
getCategories(), deleteCategory(id)The list, or whether it existed
setChannel(id, channel)The channel, on Android
getChannel(id), getChannels()Channels, on Android
deleteChannel(id)Once removed
getDevicePushToken()The push token
addPushTokenListener(listener)A subscription, on refresh
NotificationContentInputTypeDescription
titlestringThe bold first line.
stringThe bold first line.
subtitlestringA second line.
stringA second line.
bodystringThe message.
stringThe message.
dataRecord<string, unknown>Your own values, returned with the notification.
Record<string, unknown>Your own values, returned with the notification.
soundbooleanPlay the default sound.
booleanPlay the default sound.
badgenumberThe app icon badge to set on iOS.
numberThe app icon badge to set on iOS.
categoryIdentifierstringA category registered with setCategory, for its buttons.
stringA category registered with setCategory, for its buttons.
NotificationBehaviorTypeDescription
shouldShowBannerbooleanSlide in a banner.
booleanSlide in a banner.
shouldShowListbooleanKeep it in Notification Center or the shade.
booleanKeep it in Notification Center or the shade.
shouldPlaySoundbooleanPlay its sound.
booleanPlay its sound.
shouldSetBadgebooleanApply its badge, on iOS.
booleanApply its badge, on iOS.
NotificationResponseTypeDescription
notificationNotificationWhat was tapped.
NotificationWhat was tapped.
actionIdentifierstringThe button, or DEFAULT_ACTION_IDENTIFIER for the body.
stringThe button, or DEFAULT_ACTION_IDENTIFIER for the body.
userTextstringText typed into a text action.
stringText typed into a text action.
NotificationActionTypeDescription
identifierstringComes back as actionIdentifier.
stringComes back as actionIdentifier.
buttonTitlestringThe button text.
stringThe button text.
optionsNotificationActionOptionsAuthentication, destructive, and foreground flags.
NotificationActionOptionsAuthentication, destructive, and foreground flags.
textInputNotificationTextInputTurns the button into a text field.
NotificationTextInputTurns the button into a text field.
NotificationActionOptionsTypeDescription
isAuthenticationRequiredboolean = falseUnlock the device first.
boolean = falseUnlock the device first.
isDestructiveboolean = falseShow the button in red, on iOS.
boolean = falseShow the button in red, on iOS.
opensAppToForegroundboolean = trueOpen the app when tapped.
boolean = trueOpen the app when tapped.
NotificationChannelInputTypeDescription
namestringShown in Android settings.
stringShown in Android settings.
importanceNotificationImportancenone, min, low, default, high, or max.
NotificationImportancenone, min, low, default, high, or max.
descriptionstringShown under the name in settings.
stringShown under the name in settings.
soundbooleanPlay a sound.
booleanPlay a sound.
vibrationPatternnumber[]Off and on durations in milliseconds.
number[]Off and on durations in milliseconds.
showBadgebooleanAllow a launcher dot.
booleanAllow a launcher dot.
NotificationPermissionResponseTypeDescription
statusNotificationPermissionStatusgranted, denied, or undetermined.
NotificationPermissionStatusgranted, denied, or undetermined.
grantedbooleanWhether notifications can show.
booleanWhether notifications can show.
canAskAgainbooleanWhether a prompt can still appear.
booleanWhether a prompt can still appear.
ios{ status: IosAuthorizationStatus }The iOS status, including provisional.
{ status: IosAuthorizationStatus }The iOS status, including provisional.

Platform differences

iOSAndroidWeb
SchedulingThe systemInexact alarmsRejects
BadgeSet and readLauncher’s ownNone
ChannelsNoneRequiredNone
Destructive actionRed buttonSame as others
Push tokenAPNsFCMNone

Limits and gotchas

Examples

Hide banners for the chat on screen

import { useEffect } from 'react'
import { One } from 'one'
function useQuietChat(openChatId: string | null) {
useEffect(() => {
One.Notifications.setHandler({
handleNotification: (notification) => {
const chatId = notification.request.content.data.chatId
const visible = chatId !== undefined && chatId === openChatId
return {
shouldShowBanner: !visible,
shouldShowList: true,
shouldPlaySound: !visible,
shouldSetBadge: false,
}
},
})
}, [openChatId])
}

Messages for the open chat still land in the list but make no banner or sound. The handler answers synchronously, well inside the 3 second limit.

Reply from the notification

import { useEffect } from 'react'
import { One } from 'one'
async function registerReply() {
await One.Notifications.setCategory('message', [
{ identifier: 'reply', buttonTitle: 'Reply', textInput: { placeholder: 'Message' } },
{ identifier: 'read', buttonTitle: 'Mark as read', options: { opensAppToForeground: false } },
])
}
function useReplies(send: (chatId: string, text: string) => void) {
useEffect(() => {
const subscription = One.Notifications.addResponseReceivedListener((response) => {
const chatId = response.notification.request.content.data.chatId
if (response.actionIdentifier === 'reply' && typeof chatId === 'string' && response.userText) {
send(chatId, response.userText)
}
})
return () => subscription.remove()
}, [send])
}

Notifications with categoryIdentifier: 'message' get both buttons. Mark as read does not open the app, and the typed reply arrives as userText.

Open the right screen from a launching tap

import { useEffect } from 'react'
import { One, useRouter } from 'one'
function useNotificationLinks() {
const router = useRouter()
useEffect(() => {
const open = (url: unknown) => {
if (typeof url === 'string') router.push(url)
}
const launch = One.Notifications.getLastResponse()
if (launch) {
open(launch.notification.request.content.data.url)
One.Notifications.clearLastResponse()
}
const subscription = One.Notifications.addResponseReceivedListener((response) => {
open(response.notification.request.content.data.url)
})
return () => subscription.remove()
}, [router])
}

The launching tap and later taps take the same path. Clearing the stored response keeps a remount from opening the screen again.

Send the push token to your server

import { One } from 'one'
async function registerForPush(upload: (token: { type: string; data: string }) => Promise<void>) {
const { granted } = await One.Notifications.requestPermissions()
if (!granted) return
await upload(await One.Notifications.getDevicePushToken())
One.Notifications.addPushTokenListener(upload)
}

The listener fires only when the token changes later, so read the current one first. This needs notifications: { push: true } in the app config, and on Android your Firebase project file set with android.googleServicesFile.

Edit this page on GitHub.