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