1import * as Device from 'expo-device'; 2import { Subscription } from 'expo-modules-core'; 3import * as Notifications from 'expo-notifications'; 4import * as TaskManager from 'expo-task-manager'; 5import React from 'react'; 6import { Alert, Text, Platform, ScrollView, View } from 'react-native'; 7 8import registerForPushNotificationsAsync from '../api/registerForPushNotificationsAsync'; 9import HeadingText from '../components/HeadingText'; 10import ListButton from '../components/ListButton'; 11import MonoText from '../components/MonoText'; 12 13const BACKGROUND_NOTIFICATION_TASK = 'BACKGROUND-NOTIFICATION-TASK'; 14const BACKGROUND_TASK_SUCCESSFUL = 'Background task successfully ran!'; 15const BACKGROUND_TEST_INFO = `To test background notification handling:\n(1) Background the app.\n(2) Send a push notification from your terminal. The push token can be found in your logs, and the command to send a notification can be found at https://docs.expo.dev/push-notifications/sending-notifications/#http2-api. On iOS, you need to include "_contentAvailable": "true" in your payload.\n(3) After receiving the notification, check your terminal for:\n"${BACKGROUND_TASK_SUCCESSFUL}"`; 16 17TaskManager.defineTask(BACKGROUND_NOTIFICATION_TASK, (_data) => { 18 console.log(BACKGROUND_TASK_SUCCESSFUL); 19}); 20 21const remotePushSupported = Device.isDevice; 22export default class NotificationScreen extends React.Component< 23 // See: https://github.com/expo/expo/pull/10229#discussion_r490961694 24 // eslint-disable-next-line @typescript-eslint/ban-types 25 {}, 26 { 27 lastNotifications?: Notifications.Notification; 28 } 29> { 30 static navigationOptions = { 31 title: 'Notifications', 32 }; 33 34 private _onReceivedListener: Subscription | undefined; 35 private _onResponseReceivedListener: Subscription | undefined; 36 37 // See: https://github.com/expo/expo/pull/10229#discussion_r490961694 38 // eslint-disable-next-line @typescript-eslint/ban-types 39 constructor(props: {}) { 40 super(props); 41 this.state = {}; 42 } 43 44 componentDidMount() { 45 if (Platform.OS !== 'web') { 46 this._onReceivedListener = Notifications.addNotificationReceivedListener( 47 this._handelReceivedNotification 48 ); 49 this._onResponseReceivedListener = Notifications.addNotificationResponseReceivedListener( 50 this._handelNotificationResponseReceived 51 ); 52 Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK); 53 // Using the same category as in `registerForPushNotificationsAsync` 54 Notifications.setNotificationCategoryAsync('welcome', [ 55 { 56 buttonTitle: `Don't open app`, 57 identifier: 'first-button', 58 options: { 59 opensAppToForeground: false, 60 }, 61 }, 62 { 63 buttonTitle: 'Respond with text', 64 identifier: 'second-button-with-text', 65 textInput: { 66 submitButtonTitle: 'Submit button', 67 placeholder: 'Placeholder text', 68 }, 69 }, 70 { 71 buttonTitle: 'Open app', 72 identifier: 'third-button', 73 options: { 74 opensAppToForeground: true, 75 }, 76 }, 77 ]) 78 .then((_category) => {}) 79 .catch((error) => console.warn('Could not have set notification category', error)); 80 } 81 } 82 83 componentWillUnmount() { 84 this._onReceivedListener?.remove(); 85 this._onResponseReceivedListener?.remove(); 86 } 87 88 render() { 89 return ( 90 <ScrollView contentContainerStyle={{ padding: 10, paddingBottom: 40 }}> 91 <HeadingText>Local Notifications</HeadingText> 92 <ListButton 93 onPress={this._presentLocalNotificationAsync} 94 title="Present a notification immediately" 95 /> 96 <ListButton 97 onPress={this._scheduleLocalNotificationAsync} 98 title="Schedule notification for 10 seconds from now" 99 /> 100 <ListButton 101 onPress={this._scheduleLocalNotificationWithCustomSoundAsync} 102 title="Schedule notification with custom sound in 1 second (not supported in Expo Go)" 103 /> 104 <ListButton 105 onPress={this._scheduleLocalNotificationAndCancelAsync} 106 title="Schedule notification for 10 seconds from now and then cancel it immediately" 107 /> 108 <ListButton 109 onPress={Notifications.cancelAllScheduledNotificationsAsync} 110 title="Cancel all scheduled notifications" 111 /> 112 113 <HeadingText>Push Notifications</HeadingText> 114 {!remotePushSupported && ( 115 <Text> 116 ⚠️ Remote push notifications are not supported in the simulator, the following tests 117 should warn accordingly. 118 </Text> 119 )} 120 <ListButton onPress={this._sendNotificationAsync} title="Send me a push notification" /> 121 <ListButton 122 onPress={this._unregisterForNotificationsAsync} 123 title="Unregister for push notifications" 124 /> 125 <BackgroundNotificationHandlingSection /> 126 <HeadingText>Badge Number</HeadingText> 127 <ListButton 128 onPress={this._incrementIconBadgeNumberAsync} 129 title="Increment the app icon's badge number" 130 /> 131 <ListButton onPress={this._clearIconBadgeAsync} title="Clear the app icon's badge number" /> 132 133 <HeadingText>Dismissing notifications</HeadingText> 134 <ListButton 135 onPress={this._countPresentedNotifications} 136 title="Count presented notifications" 137 /> 138 <ListButton onPress={this._dismissSingle} title="Dismiss a single notification" /> 139 140 <ListButton onPress={this._dismissAll} title="Dismiss all notifications" /> 141 142 {this.state.lastNotifications && ( 143 <MonoText containerStyle={{ marginBottom: 20 }}> 144 {JSON.stringify(this.state.lastNotifications, null, 2)} 145 </MonoText> 146 )} 147 148 <HeadingText>Notification Permissions</HeadingText> 149 <ListButton onPress={this.getPermissionsAsync} title="Get permissions" /> 150 <ListButton onPress={this.requestPermissionsAsync} title="Request permissions" /> 151 152 <HeadingText>Notification triggers debugging</HeadingText> 153 <ListButton 154 onPress={() => 155 Notifications.getNextTriggerDateAsync({ seconds: 10 }).then((timestamp) => 156 alert(new Date(timestamp!)) 157 ) 158 } 159 title="Get next date for time interval + 10 seconds" 160 /> 161 <ListButton 162 onPress={() => 163 Notifications.getNextTriggerDateAsync({ 164 hour: 9, 165 minute: 0, 166 repeats: true, 167 }).then((timestamp) => alert(new Date(timestamp!))) 168 } 169 title="Get next date for 9 AM" 170 /> 171 <ListButton 172 onPress={() => 173 Notifications.getNextTriggerDateAsync({ 174 hour: 9, 175 minute: 0, 176 weekday: 1, 177 repeats: true, 178 }).then((timestamp) => alert(new Date(timestamp!))) 179 } 180 title="Get next date for Sunday, 9 AM" 181 /> 182 </ScrollView> 183 ); 184 } 185 186 _handelReceivedNotification = (notification: Notifications.Notification) => { 187 this.setState({ 188 lastNotifications: notification, 189 }); 190 }; 191 192 _handelNotificationResponseReceived = ( 193 notificationResponse: Notifications.NotificationResponse 194 ) => { 195 console.log({ notificationResponse }); 196 197 // Calling alert(message) immediately fails to show the alert on Android 198 // if after backgrounding the app and then clicking on a notification 199 // to foreground the app 200 setTimeout(() => Alert.alert('You clicked on the notification '), 1000); 201 }; 202 203 private getPermissionsAsync = async () => { 204 const permission = await Notifications.getPermissionsAsync(); 205 console.log('Get permission: ', permission); 206 alert(`Status: ${permission.status}`); 207 }; 208 209 private requestPermissionsAsync = async () => { 210 const permission = await Notifications.requestPermissionsAsync(); 211 alert(`Status: ${permission.status}`); 212 }; 213 214 _obtainUserFacingNotifPermissionsAsync = async () => { 215 let permission = await Notifications.getPermissionsAsync(); 216 if (permission.status !== 'granted') { 217 permission = await Notifications.requestPermissionsAsync(); 218 if (permission.status !== 'granted') { 219 Alert.alert(`We don't have permission to present notifications.`); 220 } 221 } 222 return permission; 223 }; 224 225 _obtainRemoteNotifPermissionsAsync = async () => { 226 let permission = await Notifications.getPermissionsAsync(); 227 if (permission.status !== 'granted') { 228 permission = await Notifications.requestPermissionsAsync(); 229 if (permission.status !== 'granted') { 230 Alert.alert(`We don't have permission to receive remote notifications.`); 231 } 232 } 233 return permission; 234 }; 235 236 _presentLocalNotificationAsync = async () => { 237 await this._obtainUserFacingNotifPermissionsAsync(); 238 await Notifications.scheduleNotificationAsync({ 239 content: { 240 title: 'Here is a scheduled notification!', 241 body: 'This is the body', 242 data: { 243 hello: 'there', 244 future: 'self', 245 }, 246 sound: true, 247 }, 248 trigger: null, 249 }); 250 }; 251 252 _scheduleLocalNotificationAsync = async () => { 253 await this._obtainUserFacingNotifPermissionsAsync(); 254 await Notifications.scheduleNotificationAsync({ 255 content: { 256 title: 'Here is a local notification!', 257 body: 'This is the body', 258 data: { 259 hello: 'there', 260 future: 'self', 261 }, 262 sound: true, 263 }, 264 trigger: { 265 seconds: 10, 266 }, 267 }); 268 }; 269 270 _scheduleLocalNotificationWithCustomSoundAsync = async () => { 271 await this._obtainUserFacingNotifPermissionsAsync(); 272 // Prepare the notification channel 273 await Notifications.setNotificationChannelAsync('custom-sound', { 274 name: 'Notification with custom sound', 275 importance: Notifications.AndroidImportance.HIGH, 276 sound: 'cat.wav', // <- for Android 8.0+ 277 }); 278 await Notifications.scheduleNotificationAsync({ 279 content: { 280 title: 'Here is a local notification!', 281 body: 'This is the body', 282 data: { 283 hello: 'there', 284 future: 'self', 285 }, 286 sound: 'cat.wav', 287 }, 288 trigger: { 289 channelId: 'custom-sound', 290 seconds: 1, 291 }, 292 }); 293 }; 294 295 _scheduleLocalNotificationAndCancelAsync = async () => { 296 await this._obtainUserFacingNotifPermissionsAsync(); 297 const notificationId = await Notifications.scheduleNotificationAsync({ 298 content: { 299 title: 'This notification should not appear', 300 body: 'It should have been cancelled. :(', 301 sound: true, 302 }, 303 trigger: { 304 seconds: 10, 305 }, 306 }); 307 await Notifications.cancelScheduledNotificationAsync(notificationId); 308 }; 309 310 _incrementIconBadgeNumberAsync = async () => { 311 const currentNumber = await Notifications.getBadgeCountAsync(); 312 await Notifications.setBadgeCountAsync(currentNumber + 1); 313 const actualNumber = await Notifications.getBadgeCountAsync(); 314 Alert.alert(`Set the badge number to ${actualNumber}`); 315 }; 316 317 _clearIconBadgeAsync = async () => { 318 await Notifications.setBadgeCountAsync(0); 319 Alert.alert(`Cleared the badge`); 320 }; 321 322 _sendNotificationAsync = async () => { 323 const permission = await this._obtainRemoteNotifPermissionsAsync(); 324 if (permission.status === 'granted') { 325 registerForPushNotificationsAsync(); 326 } 327 }; 328 329 _unregisterForNotificationsAsync = async () => { 330 try { 331 await Notifications.unregisterForNotificationsAsync(); 332 } catch (e) { 333 Alert.alert(`An error occurred un-registering for notifications: ${e}`); 334 } 335 }; 336 337 _countPresentedNotifications = async () => { 338 const presentedNotifications = await Notifications.getPresentedNotificationsAsync(); 339 Alert.alert(`You currently have ${presentedNotifications.length} notifications presented`); 340 }; 341 342 _dismissAll = async () => { 343 await Notifications.dismissAllNotificationsAsync(); 344 Alert.alert(`Notifications dismissed`); 345 }; 346 347 _dismissSingle = async () => { 348 const presentedNotifications = await Notifications.getPresentedNotificationsAsync(); 349 if (!presentedNotifications.length) { 350 Alert.alert(`No notifications to be dismissed`); 351 return; 352 } 353 354 const identifier = presentedNotifications[0].request.identifier; 355 await Notifications.dismissNotificationAsync(identifier); 356 Alert.alert(`Notification dismissed`); 357 }; 358} 359 360/** 361 * If this test is failing for you on iOS, make sure you: 362 * 363 * - Have the `remote-notification` UIBackgroundMode in app.json or info.plist 364 * - Included "_contentAvailable": "true" in your notification payload 365 * - Have "Background App Refresh" enabled in your Settings 366 * 367 * If it's still not working, try killing the rest of your active apps, since the OS 368 * may still decide not to launch the app for its own reasons. 369 */ 370function BackgroundNotificationHandlingSection() { 371 const [showInstructions, setShowInstructions] = React.useState(false); 372 373 return ( 374 <View> 375 {showInstructions ? ( 376 <View> 377 <ListButton 378 onPress={() => setShowInstructions(false)} 379 title="Hide background notification handling instructions" 380 /> 381 <MonoText>{BACKGROUND_TEST_INFO}</MonoText> 382 </View> 383 ) : ( 384 <ListButton 385 onPress={() => { 386 setShowInstructions(true); 387 getPermissionsAndLogToken(); 388 }} 389 title="Show background notification handling instructions" 390 /> 391 )} 392 </View> 393 ); 394} 395 396async function getPermissionsAndLogToken() { 397 let permission = await Notifications.getPermissionsAsync(); 398 if (permission.status !== 'granted') { 399 permission = await Notifications.requestPermissionsAsync(); 400 if (permission.status !== 'granted') { 401 Alert.alert(`We don't have permission to receive remote notifications.`); 402 } 403 } 404 if (permission.status === 'granted') { 405 const { data: token } = await Notifications.getExpoPushTokenAsync(); 406 console.log(`Got this device's push token: ${token}`); 407 } 408} 409