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