Skip to content

Commit

Permalink
feat: expo mobile notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Finn committed Sep 6, 2021
1 parent 1089af4 commit 7ef44a0
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 9 deletions.
13 changes: 12 additions & 1 deletion app.config.js
Expand Up @@ -28,6 +28,18 @@ export default {
fallbackToCacheTimeout: 0,
},
assetBundlePatterns: ['**/*'],
plugins: [
'sentry-expo',
[
'expo-notifications',
{
// icon: "./src/assets/myNotificationIcon.png",
// color: "#ffffff",
sounds: ['./src/assets/mySound.wav'],
mode: process.env.NODE_ENV,
},
],
],
ios: {
supportsTablet: true,
},
Expand All @@ -40,6 +52,5 @@ export default {
web: {
favicon: './src/assets/favicon.png',
},
plugins: ['sentry-expo'],
},
};
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -39,6 +39,7 @@
"expo-file-system": "~11.1.3",
"expo-localization": "~10.2.0",
"expo-location": "~12.1.2",
"expo-notifications": "~0.12.3",
"expo-random": "~11.2.0",
"expo-status-bar": "~1.0.4",
"expo-updates": "~0.8.2",
Expand Down
Binary file added src/assets/mySound.wav
Binary file not shown.
14 changes: 10 additions & 4 deletions src/screens/HomeScreen/HomeScreen.tsx
Expand Up @@ -11,6 +11,7 @@ import { logout } from '@/services/authentication';
import LOGGER from '@/services/logger';
import Sentry from '@/services/sentry';
import { initLocation, getLocation, getReverseGeocode } from '@/services/location';
import NotificationService from '@/services/notifications';
import { updateEndpointLocation } from '@/services/analytics';
import { SomeUtility } from '@/utilities/testUtility';

Expand All @@ -37,16 +38,20 @@ type Props = {
};

export default function HomeScreen({ navigation }: Props): JSX.Element {
const [notifications, setNotifications] = useState(new NotificationService());
const [location, setLocation] = useState<LocationObject>();
const [reverseGeocode, setReverseGeocode] = useState<SearchPlaceIndexForPositionResponse>();

useEffect(() => {
(async () => {
await notifications.registerForPushNotifications();
const locEnabled = await initLocation();
const loc = await getLocation();
setLocation(loc);
if (loc) {
setLocation(loc);
}
// TODO: If cannot access device location, use IP to Location API instead
if (locEnabled) {
if (locEnabled && loc) {
const reverseGeo = await getReverseGeocode(loc.coords);
setReverseGeocode(reverseGeo);
await updateEndpointLocation();
Expand All @@ -60,11 +65,11 @@ export default function HomeScreen({ navigation }: Props): JSX.Element {
<Text>process.env.NODE_ENV: {process.env.NODE_ENV}</Text>
<Text>process.env.NAME: {process.env.NAME}</Text>
<Text>Path Alias: {SomeUtility()}</Text>
<Text>- Location -</Text>
{/* <Text>- Location -</Text>
<Text>Latitude: {location?.coords.latitude}</Text>
<Text>Longitude: {location?.coords.longitude}</Text>
<Text>- Reverse Geocode -</Text>
<Text>{rgc?.Label}</Text>
<Text>{rgc?.Label}</Text> */}
<Button
title="Press to cause error!"
onPress={() => {
Expand All @@ -78,6 +83,7 @@ export default function HomeScreen({ navigation }: Props): JSX.Element {
Analytics.record({ name: 'buttonClick' });
}}
/>
<Button title="Schedule Local Notification" onPress={() => notifications.scheduleNotification(2, 'Hi!')} />
<Button title="Logout" onPress={() => logout()} />
{/* eslint-disable-next-line */}
<StatusBar style="auto" />
Expand Down
139 changes: 139 additions & 0 deletions src/services/notifications.ts
@@ -0,0 +1,139 @@
import { Platform } from 'react-native';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import { NotificationResponse, Notification } from 'expo-notifications';
import { Subscription } from '@unimodules/core';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { StorageKeys } from '@/utilities/constants';
import Alert from '@/utilities/alerts';
import LOGGER from '@/services/logger';

LOGGER.enable('NOTIFICATIONS');
const log = LOGGER.extend('NOTIFICATIONS');

export default class NotificationService {
EXPERIENCE_ID = process.env.EXPERIENCE_ID;
private addPushTokenListener: Subscription | null = null;
private notificationListener: Subscription;
private responseListener: Subscription;
pushToken!: string;
notification!: Notification;
notificationResponse!: NotificationResponse;

constructor() {
// Set notification handler for handling notifications (foregrounded)
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});

// Set interactive notification categories
Notifications.setNotificationCategoryAsync('YESNO', [
{ identifier: 'YES', buttonTitle: 'Yes 👍', options: { opensAppToForeground: false } },
{ identifier: 'NO', buttonTitle: 'No 👎', options: { opensAppToForeground: false } },
]);

// Add notification listener (foregrounded)
this.notificationListener = Notifications.addNotificationReceivedListener(notification => {
this.notification = notification;
});

// Add interacted notification listener (foregrounded, backgrounded, killed)
this.responseListener = Notifications.addNotificationResponseReceivedListener(async response => {
this.notificationResponse = response;
console.log(`NOTIFICATION RESPONSE ACTION: ${response.actionIdentifier}`);

// Dismiss notification
Notifications.dismissNotificationAsync(response.notification.request.identifier);
});

log.info('Notification Service initialized');
}

async registerForPushNotifications(): Promise<void> {
let token;
if (Constants.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
log.error('Permission not granted');
Alert('Notification Error', 'Failed to get push token for push notification');
}
try {
token = (await Notifications.getExpoPushTokenAsync({ experienceId: this.EXPERIENCE_ID })).data;
log.debug(token);
await this.savePostTokenToDB(token);
} catch (error) {
log.error(error);
Alert('Notification Error', 'Failed to register push token');
}
} else {
log.error('Must use physical device for Push Notifications');
Alert('Notification Error', 'Must use physical device for Push Notifications');
}

if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
sound: 'mySound.wav',
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}

this.pushToken = token || '';

// Setup token refresh listener
this.addPushTokenListener = Notifications.addPushTokenListener(this.registerForPushNotifications);
}

private async savePostTokenToDB(token: string): Promise<void> {
const type = Platform.OS;
try {
// Retrieve and parse token (if exists)
const storedToken = await AsyncStorage.getItem(StorageKeys.PUSH_TOKEN);

// First time saving token -> create record
if (!storedToken) {
// TODO: create database record and store token locally
}

// Stored token doesn't match current token -> update record
if (storedToken !== token) {
// TODO: update database record and store token locally
}
} catch (error) {
log.error(error);
Alert('Notification Error', 'Failed to update push token');
}
}

async scheduleNotification(seconds: number, title: string): Promise<void> {
await Notifications.scheduleNotificationAsync({
content: {
title,
},
trigger: {
seconds,
channelId: 'default',
},
});
}

destructor(): void {
if (this.addPushTokenListener) {
this.addPushTokenListener.remove();
}

this.notificationListener.remove();
this.responseListener.remove();
}
}
1 change: 1 addition & 0 deletions src/utilities/constants.ts
Expand Up @@ -2,4 +2,5 @@ export const StorageKeys = {
UID: 'uid',
REVERSE_GEOCODE: 'reverse-geocode',
PASSWORD_KEY: 'password-key',
PUSH_TOKEN: 'push-token',
};

0 comments on commit 7ef44a0

Please sign in to comment.