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 <BackgroundNotificationHandlingSection /> 122 <HeadingText>Badge Number</HeadingText> 123 <ListButton 124 onPress={this._incrementIconBadgeNumberAsync} 125 title="Increment the app icon's badge number" 126 /> 127 <ListButton onPress={this._clearIconBadgeAsync} title="Clear the app icon's badge number" /> 128 129 <HeadingText>Dismissing notifications</HeadingText> 130 <ListButton 131 onPress={this._countPresentedNotifications} 132 title="Count presented notifications" 133 /> 134 <ListButton onPress={this._dismissSingle} title="Dismiss a single notification" /> 135 136 <ListButton onPress={this._dismissAll} title="Dismiss all notifications" /> 137 138 {this.state.lastNotifications && ( 139 <MonoText containerStyle={{ marginBottom: 20 }}> 140 {JSON.stringify(this.state.lastNotifications, null, 2)} 141 </MonoText> 142 )} 143 144 <HeadingText>Notification Permissions</HeadingText> 145 <ListButton onPress={this.getPermissionsAsync} title="Get permissions" /> 146 <ListButton onPress={this.requestPermissionsAsync} title="Request permissions" /> 147 148 <HeadingText>Notification triggers debugging</HeadingText> 149 <ListButton 150 onPress={() => 151 Notifications.getNextTriggerDateAsync({ seconds: 10 }).then((timestamp) => 152 alert(new Date(timestamp!)) 153 ) 154 } 155 title="Get next date for time interval + 10 seconds" 156 /> 157 <ListButton 158 onPress={() => 159 Notifications.getNextTriggerDateAsync({ 160 hour: 9, 161 minute: 0, 162 repeats: true, 163 }).then((timestamp) => alert(new Date(timestamp!))) 164 } 165 title="Get next date for 9 AM" 166 /> 167 <ListButton 168 onPress={() => 169 Notifications.getNextTriggerDateAsync({ 170 hour: 9, 171 minute: 0, 172 weekday: 1, 173 repeats: true, 174 }).then((timestamp) => alert(new Date(timestamp!))) 175 } 176 title="Get next date for Sunday, 9 AM" 177 /> 178 </ScrollView> 179 ); 180 } 181 182 _handelReceivedNotification = (notification: Notifications.Notification) => { 183 this.setState({ 184 lastNotifications: notification, 185 }); 186 }; 187 188 _handelNotificationResponseReceived = ( 189 notificationResponse: Notifications.NotificationResponse 190 ) => { 191 console.log({ notificationResponse }); 192 193 // Calling alert(message) immediately fails to show the alert on Android 194 // if after backgrounding the app and then clicking on a notification 195 // to foreground the app 196 setTimeout(() => Alert.alert('You clicked on the notification '), 1000); 197 }; 198 199 private getPermissionsAsync = async () => { 200 const permission = await Notifications.getPermissionsAsync(); 201 console.log('Get permission: ', permission); 202 alert(`Status: ${permission.status}`); 203 }; 204 205 private requestPermissionsAsync = async () => { 206 const permission = await Notifications.requestPermissionsAsync(); 207 alert(`Status: ${permission.status}`); 208 }; 209 210 _obtainUserFacingNotifPermissionsAsync = async () => { 211 let permission = await Notifications.getPermissionsAsync(); 212 if (permission.status !== 'granted') { 213 permission = await Notifications.requestPermissionsAsync(); 214 if (permission.status !== 'granted') { 215 Alert.alert(`We don't have permission to present notifications.`); 216 } 217 } 218 return permission; 219 }; 220 221 _obtainRemoteNotifPermissionsAsync = async () => { 222 let permission = await Notifications.getPermissionsAsync(); 223 if (permission.status !== 'granted') { 224 permission = await Notifications.requestPermissionsAsync(); 225 if (permission.status !== 'granted') { 226 Alert.alert(`We don't have permission to receive remote notifications.`); 227 } 228 } 229 return permission; 230 }; 231 232 _presentLocalNotificationAsync = async () => { 233 await this._obtainUserFacingNotifPermissionsAsync(); 234 await Notifications.scheduleNotificationAsync({ 235 content: { 236 title: 'Here is a scheduled notification!', 237 body: 'This is the body', 238 data: { 239 hello: 'there', 240 future: 'self', 241 }, 242 sound: true, 243 }, 244 trigger: null, 245 }); 246 }; 247 248 _scheduleLocalNotificationAsync = async () => { 249 await this._obtainUserFacingNotifPermissionsAsync(); 250 await Notifications.scheduleNotificationAsync({ 251 content: { 252 title: 'Here is a local notification!', 253 body: 'This is the body', 254 data: { 255 hello: 'there', 256 future: 'self', 257 }, 258 sound: true, 259 }, 260 trigger: { 261 seconds: 10, 262 }, 263 }); 264 }; 265 266 _scheduleLocalNotificationWithCustomSoundAsync = async () => { 267 await this._obtainUserFacingNotifPermissionsAsync(); 268 // Prepare the notification channel 269 await Notifications.setNotificationChannelAsync('custom-sound', { 270 name: 'Notification with custom sound', 271 importance: Notifications.AndroidImportance.HIGH, 272 sound: 'cat.wav', // <- for Android 8.0+ 273 }); 274 await Notifications.scheduleNotificationAsync({ 275 content: { 276 title: 'Here is a local notification!', 277 body: 'This is the body', 278 data: { 279 hello: 'there', 280 future: 'self', 281 }, 282 sound: 'cat.wav', 283 }, 284 trigger: { 285 channelId: 'custom-sound', 286 seconds: 1, 287 }, 288 }); 289 }; 290 291 _scheduleLocalNotificationAndCancelAsync = async () => { 292 await this._obtainUserFacingNotifPermissionsAsync(); 293 const notificationId = await Notifications.scheduleNotificationAsync({ 294 content: { 295 title: 'This notification should not appear', 296 body: 'It should have been cancelled. :(', 297 sound: true, 298 }, 299 trigger: { 300 seconds: 10, 301 }, 302 }); 303 await Notifications.cancelScheduledNotificationAsync(notificationId); 304 }; 305 306 _incrementIconBadgeNumberAsync = async () => { 307 const currentNumber = await Notifications.getBadgeCountAsync(); 308 await Notifications.setBadgeCountAsync(currentNumber + 1); 309 const actualNumber = await Notifications.getBadgeCountAsync(); 310 Alert.alert(`Set the badge number to ${actualNumber}`); 311 }; 312 313 _clearIconBadgeAsync = async () => { 314 await Notifications.setBadgeCountAsync(0); 315 Alert.alert(`Cleared the badge`); 316 }; 317 318 _sendNotificationAsync = async () => { 319 const permission = await this._obtainRemoteNotifPermissionsAsync(); 320 if (permission.status === 'granted') { 321 registerForPushNotificationsAsync(); 322 } 323 }; 324 325 _countPresentedNotifications = async () => { 326 const presentedNotifications = await Notifications.getPresentedNotificationsAsync(); 327 Alert.alert(`You currently have ${presentedNotifications.length} notifications presented`); 328 }; 329 330 _dismissAll = async () => { 331 await Notifications.dismissAllNotificationsAsync(); 332 Alert.alert(`Notifications dismissed`); 333 }; 334 335 _dismissSingle = async () => { 336 const presentedNotifications = await Notifications.getPresentedNotificationsAsync(); 337 if (!presentedNotifications.length) { 338 Alert.alert(`No notifications to be dismissed`); 339 return; 340 } 341 342 const identifier = presentedNotifications[0].request.identifier; 343 await Notifications.dismissNotificationAsync(identifier); 344 Alert.alert(`Notification dismissed`); 345 }; 346} 347 348/** 349 * If this test is failing for you on iOS, make sure you: 350 * 351 * - Have the `remote-notification` UIBackgroundMode in app.json or info.plist 352 * - Included "_contentAvailable": "true" in your notification payload 353 * - Have "Background App Refresh" enabled in your Settings 354 * 355 * If it's still not working, try killing the rest of your active apps, since the OS 356 * may still decide not to launch the app for its own reasons. 357 */ 358function BackgroundNotificationHandlingSection() { 359 const [showInstructions, setShowInstructions] = React.useState(false); 360 361 return ( 362 <View> 363 {showInstructions ? ( 364 <View> 365 <ListButton 366 onPress={() => setShowInstructions(false)} 367 title="Hide background notification handling instructions" 368 /> 369 <MonoText>{BACKGROUND_TEST_INFO}</MonoText> 370 </View> 371 ) : ( 372 <ListButton 373 onPress={() => { 374 setShowInstructions(true); 375 getPermissionsAndLogToken(); 376 }} 377 title="Show background notification handling instructions" 378 /> 379 )} 380 </View> 381 ); 382} 383 384async function getPermissionsAndLogToken() { 385 let permission = await Notifications.getPermissionsAsync(); 386 if (permission.status !== 'granted') { 387 permission = await Notifications.requestPermissionsAsync(); 388 if (permission.status !== 'granted') { 389 Alert.alert(`We don't have permission to receive remote notifications.`); 390 } 391 } 392 if (permission.status === 'granted') { 393 const { data: token } = await Notifications.getExpoPushTokenAsync(); 394 console.log(`Got this device's push token: ${token}`); 395 } 396} 397