Local notifications and remote push tokens through One.Notifications.
Platform enums are string unions.
import { One } from 'one'
const current = await One.Notifications.getPermissions()if (!current.granted) { await One.Notifications.requestPermissions()}await One.Notifications.setBadgeCount(3)Permission reads resolve
{ status: 'granted' | 'denied' | 'undetermined', granted, canAskAgain },
with an ios.status carrying the authorization as
'notDetermined' | 'denied' | 'authorized' | 'provisional' | 'ephemeral'
on iOS. requestPermissions accepts
{ ios: { allowAlert, allowBadge, allowSound, allowProvisional } }; unset
fields default to true except allowProvisional. On Android below 13 there is
no runtime prompt, so requesting resolves the current status. setBadgeCount
resolves false and getBadgeCount resolves 0 on Android: the launcher
owns badges there.
Android delivery targets notification channels. Create one before scheduling onto it; settings apply on first create, afterwards the user owns them in system settings.
await One.Notifications.setChannel('reminders', { name: 'Reminders', importance: 'default',})getChannel, getChannels, and deleteChannel round out the set. On iOS
they resolve null, [], and nothing: there are no channels there.
Importance is 'none' | 'min' | 'low' | 'default' | 'high' | 'max', and
the numbers 0 to 5 map to those in order.
schedule delivers a local notification; trigger: null shows it
immediately. Every arrival fires addReceivedListener while the app runs in
the foreground, and every tap fires addResponseReceivedListener with the
notification and the default action identifier:
One.Notifications.setHandler({ handleNotification: async () => ({ shouldShowBanner: true, shouldShowList: true, shouldPlaySound: false, shouldSetBadge: false, }),})
const id = await One.Notifications.schedule({ content: { title: 'Hello', body: 'world' }, trigger: null,})Until the app sets a handler the notification shows fully: banner, list,
sound, and badge. If a handler stalls, native presents after a 3 s backstop
rather than dropping the notification; JS itself never times out, so a late
answer from a stalled handler is a no-op. With no listeners mounted at all
native presents immediately instead of waiting out the backstop. Setting the handler to null leaves native
undecided, which hides the notification on both platforms while listeners
still fire. On Android the banner flag raises the priority but the channel
importance governs what the system actually shows, and shouldSetBadge is
ignored: launcher badges are out of scope.
A tap that cold-starts the app cannot reach a listener: JS is not running
yet. It surfaces through the synchronous getLastResponse instead, which
the app reads on launch; clearLastResponse dismisses it. The iOS delegate
installs at launch, before React Native starts, so the cold-start tap is cached
no matter how early it arrives.
Two more triggers schedule into the future: { type: 'timeInterval', seconds, repeats? } and { type: 'date', date }, where the date is a timestamp or a
Date. A past date delivers immediately. Both take an Android channelId;
unknown ids fall back to an owned Default channel rather than posting
nowhere. Rescheduling an identifier replaces it.
await One.Notifications.schedule({ identifier: 'tea', content: { title: 'Tea', body: 'ready' }, trigger: { type: 'timeInterval', seconds: 300 },})cancelScheduled removes one pending notification by identifier,
cancelAllScheduled clears the pending list, and getAllScheduled
resolves the pending requests. getPresented resolves the notifications
currently on screen, dismiss removes one by identifier, and dismissAll
clears them.
On web there is no native runtime: permission reads resolve denied, lists
are empty, listeners are inert, and schedule rejects. Input checks match
native exactly, so bad input throws on web the same way.
On Android the triggers ride AlarmManager.setAndAllowWhileIdle: one inexact
path, no SCHEDULE_EXACT_ALARM, so a delivery deferred by doze is expected,
not a bug. Schedules persist across restarts and a boot receiver re-arms
them; one-shots that expired while the device was off are dropped and repeats
restart from boot. iOS has two platform rules worth knowing: a repeating
interval must be at least 60 seconds, and the calendar trigger resolves to
whole seconds.
getDevicePushToken resolves { type: 'ios' | 'android', data }: the
APNs hex token on iOS (via registerForRemoteNotifications, which works on
the simulator on iOS 16+) and the FCM registration token on Android.
addPushTokenListener returns a subscription that fires when the token
refreshes:
const token = await One.Notifications.getDevicePushToken()// { type: 'ios', data: 'a1b2...' }
const sub = One.Notifications.addPushTokenListener((next) => { console.log(next.data)})sub.remove()Push is opt-in per platform. On Android it needs
native.app.notifications.push, which compiles the Firebase Messaging
source set in; without it the app carries no Firebase classes and the call
rejects. On iOS the same flag stamps the aps-environment entitlement;
without it the call rejects as well, since the OS never answers the
registration. One receives the token from the app delegate itself, so the
host needs no delegate code.
Declare the capability in native.app so prebuild stamps the Android
permissions and receiver plus the iOS delegate gate. Without it the iOS
delegate never installs, so an app that uses its own push library keeps
that library’s delegate.
Expo-prebuilt apps pass the same choice to vxrn/expo-plugin, which writes
the same entries during expo prebuild. mode sets aps-environment, as
in expo-notifications, and defaults to development:
{ "expo": { "plugins": [ ["vxrn/expo-plugin", { "notifications": { "push": true, "mode": "production" } }] ] }}Use { "notifications": {} } for local notifications without push.
Edit this page on GitHub.