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