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.
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.
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.
Permission and badge
| Method | Resolves |
|---|---|
| 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
| Method | Resolves |
|---|---|
| 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
| Method | Returns |
|---|---|
| setHandler | Nothing |
| addReceivedListener | A subscription |
| addResponseReceivedListener | A subscription |
| getLastResponse | The launching tap, or null |
| clearLastResponse | Nothing |
Categories, channels, and push
| Method | Resolves |
|---|---|
| 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 |
| NotificationContentInput | Type | Description |
|---|---|---|
titlestringThe bold first line. | string | The bold first line. |
subtitlestringA second line. | string | A second line. |
bodystringThe message. | string | The 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. | boolean | Play the default sound. |
badgenumberThe app icon badge to set on iOS. | number | The app icon badge to set on iOS. |
categoryIdentifierstringA category registered with setCategory, for its buttons. | string | A category registered with setCategory, for its buttons. |
| NotificationBehavior | Type | Description |
|---|---|---|
shouldShowBannerbooleanSlide in a banner. | boolean | Slide in a banner. |
shouldShowListbooleanKeep it in Notification Center or the shade. | boolean | Keep it in Notification Center or the shade. |
shouldPlaySoundbooleanPlay its sound. | boolean | Play its sound. |
shouldSetBadgebooleanApply its badge, on iOS. | boolean | Apply its badge, on iOS. |
| NotificationResponse | Type | Description |
|---|---|---|
notificationNotificationWhat was tapped. | Notification | What was tapped. |
actionIdentifierstringThe button, or DEFAULT_ACTION_IDENTIFIER for the body. | string | The button, or DEFAULT_ACTION_IDENTIFIER for the body. |
userTextstringText typed into a text action. | string | Text typed into a text action. |
| NotificationAction | Type | Description |
|---|---|---|
identifierstringComes back as actionIdentifier. | string | Comes back as actionIdentifier. |
buttonTitlestringThe button text. | string | The button text. |
optionsNotificationActionOptionsAuthentication, destructive, and foreground flags. | NotificationActionOptions | Authentication, destructive, and foreground flags. |
textInputNotificationTextInputTurns the button into a text field. | NotificationTextInput | Turns the button into a text field. |
| NotificationActionOptions | Type | Description |
|---|---|---|
isAuthenticationRequiredboolean = falseUnlock the device first. | boolean = false | Unlock the device first. |
isDestructiveboolean = falseShow the button in red, on iOS. | boolean = false | Show the button in red, on iOS. |
opensAppToForegroundboolean = trueOpen the app when tapped. | boolean = true | Open the app when tapped. |
| NotificationChannelInput | Type | Description |
|---|---|---|
namestringShown in Android settings. | string | Shown in Android settings. |
importanceNotificationImportancenone, min, low, default, high, or max. | NotificationImportance | none, min, low, default, high, or max. |
descriptionstringShown under the name in settings. | string | Shown under the name in settings. |
soundbooleanPlay a sound. | boolean | Play a sound. |
vibrationPatternnumber[]Off and on durations in milliseconds. | number[] | Off and on durations in milliseconds. |
showBadgebooleanAllow a launcher dot. | boolean | Allow a launcher dot. |
| NotificationPermissionResponse | Type | Description |
|---|---|---|
statusNotificationPermissionStatusgranted, denied, or undetermined. | NotificationPermissionStatus | granted, denied, or undetermined. |
grantedbooleanWhether notifications can show. | boolean | Whether notifications can show. |
canAskAgainbooleanWhether a prompt can still appear. | boolean | Whether a prompt can still appear. |
ios{ status: IosAuthorizationStatus }The iOS status, including provisional. | { status: IosAuthorizationStatus } | The iOS status, including provisional. |
| iOS | Android | Web | |
|---|---|---|---|
| Scheduling | The system | Inexact alarms | Rejects |
| Badge | Set and read | Launcher’s own | None |
| Channels | None | Required | None |
| Destructive action | Red button | Same as others | |
| Push token | APNs | FCM | None |
setAndAllowWhileIdle, which
needs no exact alarm permission, so doze can delay a reminder by minutes.
Do not use it for alarm clock timing.E_NOTIFICATIONS_TRIGGER.channelId posts to a channel named
Default that One creates. A channel’s settings apply when it is created;
after that people own them in system settings, and setChannel cannot
change them.shouldShowBanner raises the
notification’s priority, but the channel’s importance controls whether a
banner appears. shouldSetBadge does nothing there.getLastResponse once JavaScript starts, then
clearLastResponse so you do not handle it twice.: or -; anything else throws, the same on every platform. A
notification whose category is unknown posts without buttons.data carries plain JSON. Strings, numbers, booleans, null, arrays,
and objects pass through. Functions and undefined are dropped, and a
Date arrives as an empty object, so send timestamps as numbers.development
environment unless apsEnvironment is production, which builds for
TestFlight and the App Store need.schedule rejects. Arguments are still checked,
so bad input throws on web too.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.
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.
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.
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.