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