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