UNPKG

react-native-ios-alarmkit

Version:
463 lines (338 loc) 12.8 kB
# 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