react-native-ios-alarmkit
Version:
React Native wrapper for iOS AlarmKit framework
463 lines (338 loc) • 12.8 kB
Markdown
# react-native-ios-alarmkit
React Native wrapper for iOS AlarmKit framework. Schedule alarms, timers, and countdown alerts with Live Activities on iOS 26+.
## Features
- Simple API for quick timer/alarm scheduling
- Advanced API with 1:1 native AlarmKit mapping
- React Hooks for automatic state updates
- Event listeners for real-time alarm changes
- Custom alert presentations with buttons, colors, and SF Symbols
- Live Activities support
- Silent no-op on Android and iOS < 26
## Installation
```bash
yarn add react-native-ios-alarmkit react-native-nitro-modules
```
### iOS Setup
1. Add to `ios/YourApp/Info.plist`:
```xml
<key>NSAlarmKitUsageDescription</key>
<string>This app needs to schedule alarms to remind you at important times.</string>
```
> **Required.** If missing or empty, AlarmKit cannot schedule alarms.
2. Install pods:
```bash
cd ios && pod install
```
AlarmKit only works on iOS 26+. On older versions, `AlarmKit.isSupported` returns `false` and all methods are silent no-ops. Your app can target iOS 15.1+.
### Android
No setup required. Returns `isSupported: false`.
### Expo
Requires native code. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) with `npx expo prebuild`.
## Usage
### Simple API
```typescript
import AlarmKit from 'react-native-ios-alarmkit'
// Check support
if (!AlarmKit.isSupported) {
console.log('AlarmKit not supported')
}
// Request authorization
const authorized = await AlarmKit.requestAuthorization()
// Schedule a timer (5 minutes)
const timerId = crypto.randomUUID()
await AlarmKit.scheduleTimer(timerId, {
duration: 300,
title: 'Timer Done!',
snoozeEnabled: true,
snoozeDuration: 60,
tintColor: '#FF6B6B',
})
// Schedule a recurring alarm
// Scheduling with same ID again - the lib first calls delete() and then creates a new one
const alarmId = crypto.randomUUID()
await AlarmKit.scheduleAlarm(alarmId, {
hour: 7,
minute: 0,
weekdays: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
title: 'Wake Up!',
snoozeEnabled: false,
tintColor: '#4A90D9',
})
// Get all alarms
const alarms = await AlarmKit.getAlarms()
// Control alarms
await AlarmKit.cancel(timerId)
await AlarmKit.pause(timerId)
await AlarmKit.resume(timerId)
```
### React Hooks
Hooks return objects with `error` and `isLoading` states:
```typescript
import { useAlarms, useAuthorizationState } from 'react-native-ios-alarmkit'
function MyComponent() {
const { state: authState, error: authError, isLoading: authLoading } = useAuthorizationState()
const { alarms, error: alarmsError, isLoading: alarmsLoading } = useAlarms()
if (authLoading || alarmsLoading) {
return <Text>Loading...</Text>
}
if (authError || alarmsError) {
return <Text>Error: {authError?.message || alarmsError?.message}</Text>
}
return (
<View>
<Text>Authorization: {authState}</Text>
<Text>Active Alarms: {alarms.length}</Text>
{alarms.map((alarm) => (
<Text key={alarm.id}>
{alarm.id} - {alarm.state}
</Text>
))}
</View>
)
}
```
### Event Listeners
```typescript
const alarmsSub = AlarmKit.addAlarmsListener((alarms) => {
console.log('Alarms updated:', alarms)
})
const authSub = AlarmKit.addAuthorizationListener((state) => {
console.log('Authorization changed:', state)
})
// Cleanup
alarmsSub.remove()
authSub.remove()
```
## Widget Extension / Live Activities Requirements
A Widget Extension is **required** for the following features:
| Feature | Widget Extension Required? | Notes |
| ------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------- |
| `AlarmKit.scheduleAlarm()` | **No** | Works without Widget Extension - basic alarm functionality |
| `AlarmKit.scheduleTimer()` | **Yes** | Timers show countdown UI in Dynamic Island, Lock Screen, and StandBy |
| Snooze with countdown UI | **Yes (recommended)** | Without Widget Extension, snooze may work but countdown UI won't display properly |
| Advanced configs with countdown | **Yes** | Custom alarms using `AlarmKitManager.shared.schedule()` with `countdown` or `paused` presentations |
**What happens without Widget Extension:**
- `scheduleAlarm()`: Works correctly - alarms alert at scheduled time
- `scheduleTimer()`: iOS may unexpectedly dismiss timers and fail to alert
- Countdown presentations: System may not show Live Activity and dismiss alarms ([reference](https://developer.apple.com/documentation/alarmkit/scheduling-an-alarm-with-alarmkit#Create-a-Widget-Extension-for-Live-Activities))
**For advanced configurations** with countdown presentations (pre-alert countdown, custom timers, etc.), use `AlarmKitManager.shared.schedule()` directly with full `AlarmConfiguration` and ensure your app has a Widget Extension.
See [Live Activity Setup Guide](./docs/LIVE_ACTIVITY_SETUP.md) for implementation details.
### Advanced API
Full control with 1:1 native AlarmKit mapping:
```typescript
import {
AlarmKitManager,
AlarmConfigurationFactory,
} from 'react-native-ios-alarmkit'
const config = AlarmConfigurationFactory.timer({
duration: 300,
attributes: {
presentation: {
alert: {
title: 'Timer Done!',
stopButton: {
text: 'Stop',
textColor: '#FFFFFF',
systemImageName: 'stop.circle',
},
secondaryButton: {
text: 'Snooze',
textColor: '#FFFFFF',
systemImageName: 'zzz',
},
secondaryButtonBehavior: 'countdown',
},
countdown: {
title: 'Time Remaining',
pauseButton: {
text: 'Cancel',
textColor: '#FF6B6B',
systemImageName: 'xmark.circle',
},
},
paused: {
title: 'Paused',
resumeButton: {
text: 'Resume',
textColor: '#4A90D9',
systemImageName: 'play.circle',
},
},
},
tintColor: '#FF6B6B',
metadata: {
customKey: 'customValue',
},
},
sound: 'custom-sound',
})
const id = crypto.randomUUID()
await AlarmKitManager.shared.schedule(id, config)
await AlarmKitManager.shared.countdown(id)
const alarms = await AlarmKitManager.shared.getAlarms()
// Listeners (note: method names differ from Simple API)
const sub = AlarmKitManager.shared.addAlarmUpdatesListener((alarms) => {
console.log('Alarms updated:', alarms)
})
sub.remove()
```
## API Reference
### Simple API
#### `AlarmKit.isSupported: boolean`
`true` if AlarmKit is available (iOS 26+).
#### `AlarmKit.getAuthorizationState(): Promise<AuthorizationState>`
Returns `'notDetermined'`, `'authorized'`, or `'denied'`.
#### `AlarmKit.requestAuthorization(): Promise<boolean>`
Request permission. Returns `true` if granted. If not called, AlarmKit auto-requests on first `schedule()`.
#### `AlarmKit.scheduleTimer(id: string, config: SimpleTimerConfig): Promise<void>`
Schedule a countdown timer. `id` must be a valid UUID.
#### `AlarmKit.scheduleAlarm(id: string, config: SimpleAlarmConfig): Promise<void>`
Schedule a recurring alarm. `id` must be a valid UUID.
#### `AlarmKit.cancel(id: string): Promise<void>`
Cancel a scheduled alarm.
#### `AlarmKit.stop(id: string): Promise<void>`
Stop an alerting alarm.
#### `AlarmKit.pause(id: string): Promise<void>`
Pause a countdown.
#### `AlarmKit.resume(id: string): Promise<void>`
Resume a paused countdown.
#### `AlarmKit.countdown(id: string): Promise<void>`
Start countdown for a scheduled alarm.
#### `AlarmKit.getAlarms(): Promise<Alarm[]>`
Get all active alarms.
#### `AlarmKit.addAlarmsListener(callback): Subscription`
Subscribe to alarm changes.
#### `AlarmKit.addAuthorizationListener(callback): Subscription`
Subscribe to authorization changes.
### Hooks
#### `useAlarms(): UseAlarmsResult`
```typescript
interface UseAlarmsResult {
alarms: Alarm[]
error: AlarmKitError | null
isLoading: boolean
}
```
#### `useAuthorizationState(): UseAuthorizationResult`
```typescript
interface UseAuthorizationResult {
state: AuthorizationState
error: AlarmKitError | null
isLoading: boolean
}
```
### Advanced API
#### `AlarmKitManager.shared`
Singleton mirroring native `AlarmManager.shared`.
Methods: `schedule`, `cancel`, `stop`, `pause`, `resume`, `countdown`, `getAlarms`, `getAuthorizationState`, `requestAuthorization`.
Listeners:
- `addAlarmUpdatesListener(callback)` - alarm changes
- `addAuthorizationUpdatesListener(callback)` - auth changes
### Factory Methods
#### `AlarmConfigurationFactory.timer(options): AlarmConfiguration`
Timer-only configuration.
#### `AlarmConfigurationFactory.alarm(options): AlarmConfiguration`
Alarm-only configuration.
#### `AlarmConfigurationFactory.create(options): AlarmConfiguration`
Full configuration with both countdown and schedule.
### Types
```typescript
type AuthorizationState = 'notDetermined' | 'authorized' | 'denied'
type AlarmState = 'scheduled' | 'countdown' | 'paused' | 'alerting'
type Weekday =
| 'sunday'
| 'monday'
| 'tuesday'
| 'wednesday'
| 'thursday'
| 'friday'
| 'saturday'
interface Alarm {
id: string // UUID
state: AlarmState
countdownDuration: CountdownDuration | null
schedule: AlarmSchedule | null
}
interface SimpleTimerConfig {
duration: number // seconds
title: string
snoozeEnabled?: boolean
snoozeDuration?: number // seconds, default 300
tintColor?: string // hex color
sound?: string // custom sound filename
}
interface SimpleAlarmConfig {
hour: number // 0-23
minute: number // 0-59
weekdays?: Weekday[] // omit for daily
title: string
snoozeEnabled?: boolean
snoozeDuration?: number // seconds, default 540
tintColor?: string
sound?: string
}
interface AlarmButton {
text: string
textColor: string
systemImageName: string
}
interface AlarmPresentation {
alert: AlertPresentation
countdown?: CountdownPresentation
paused?: PausedPresentation
}
interface Subscription {
remove: () => void
}
```
## Errors
All AlarmKit methods throw `AlarmKitError` on failure, with structured error codes for programmatic handling:
```typescript
import {
AlarmKit,
AlarmKitError,
AlarmKitErrorCode,
} from 'react-native-ios-alarmkit'
try {
await AlarmKit.cancel(alarmId)
} catch (error) {
if (error instanceof AlarmKitError) {
console.log('Error code:', error.code)
console.log('Error message:', error.message)
console.log('Native error:', error.nativeError)
// Check specific error types
if (error.code === AlarmKitErrorCode.ALARM_NOT_FOUND) {
console.log('Alarm does not exist')
} else if (error.code === AlarmKitErrorCode.MAXIMUM_LIMIT_REACHED) {
console.log('Too many alarms scheduled')
}
}
}
```
### Error Codes
| Code | Description |
| ----------------------- | ---------------------------------------------- |
| `INVALID_UUID` | The alarm ID is not a valid UUID |
| `INVALID_JSON` | Configuration JSON is malformed |
| `INVALID_CONFIGURATION` | Configuration validation failed |
| `ALARM_NOT_FOUND` | Alarm doesn't exist (cancel/stop/pause/resume) |
| `MAXIMUM_LIMIT_REACHED` | iOS alarm limit reached |
| `UNAUTHORIZED` | User denied alarm permission |
| `ALARM_EXISTS` | Alarm with this ID already exists |
| `UNKNOWN` | Unrecognized error from iOS |
### `maximumLimitReached`
iOS limits the number of scheduled alarms per app. If you hit this limit, `schedule()` throws. Cancel unused alarms before scheduling new ones.
### `Invalid UUID`
The `id` parameter must be a valid UUID string. Use `crypto.randomUUID()`.
## Platform Support
| Platform | Support | Notes |
| ----------- | -------------------- | --------------------------- |
| iOS 26+ | Full | All features available |
| iOS 15.1-25 | Compiles | `isSupported: false`, no-op |
| iOS < 15.1 | Not supported | Library requires iOS 15.1+ |
| Android | `isSupported: false` | No-op, no crashes |
Your app does not need iOS 26 as minimum target. Runtime checks handle older versions.
## Example
See [example](./example) for a complete demo.
## License
MIT