Skip to content
This repository has been archived by the owner on Dec 22, 2023. It is now read-only.

Commit

Permalink
Merge pull request #27 from UKDanceBlue/notifications-and-linking
Browse files Browse the repository at this point in the history
DEV-68 Notifications and linking
  • Loading branch information
jthoward64 authored Dec 25, 2021
2 parents 0e56d2f + ceb7662 commit 18c2822
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 59 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"expo-asset": "~8.4.4",
"expo-blur": "~11.0.0",
"expo-calendar": "~10.1.0",
"expo-constants": "~13.0.0",
"expo-device": "~4.1.0",
"expo-font": "~10.0.4",
"expo-linking": "~3.0.0",
"expo-notifications": "~0.14.0",
Expand Down
149 changes: 117 additions & 32 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Import third-party dependencies
import { registerRootComponent } from 'expo';
import React, { useEffect } from 'react';
import { StatusBar, LogBox, Platform } from 'react-native';
import { StatusBar, LogBox, Linking } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import Constants from 'expo-constants';
import * as SecureStore from 'expo-secure-store';
import * as Notifications from 'expo-notifications';
import * as Random from 'expo-random';
import * as Device from 'expo-device';

// Import Firebase Context Provider
import { doc, setDoc } from 'firebase/firestore';
Expand All @@ -17,6 +17,7 @@ import { globalColors } from './theme';

import { firebaseAuth, firebaseFirestore } from './common/FirebaseApp';

// Block the pop-up error box in dev-mode until firebase finds a way to remove the old AsyncStorage
LogBox.ignoreLogs([
`AsyncStorage has been extracted from react-native core and will be removed in a future release`,
]);
Expand Down Expand Up @@ -82,7 +83,10 @@ const App = () => {
observerUnsubscribe = onAuthStateChanged(firebaseAuth, (newUser) =>
setDoc(
deviceRef,
{ latestUserId: newUser.uid, audiences: ['all', ...newUser.audiences] },
{
latestUserId: newUser.uid,
audiences: newUser.attributes ? ['all', ...newUser.attributes] : ['all'],
},
{ mergeFields: ['latestUserId', 'audiences'] }
).catch(handleFirebaeError)
);
Expand All @@ -100,37 +104,51 @@ const App = () => {
* Register notification support with the OS and get a token from expo
*/
const registerForPushNotificationsAsync = async () => {
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: globalColors.red,
});
}

if (Constants.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
try {
if (Device.osName === 'Android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: globalColors.red,
});
}
if (Platform.OS === 'ios' && finalStatus === 1) finalStatus = 'granted';
if (finalStatus !== 'granted') {
showMessage(
'Failed to get push token for push notification!',
'Error',
() => {},
true,
`Final Status: ${finalStatus}`
);
return undefined;

if (Device.isDevice) {
// Get the user's current preference
let settings = await Notifications.getPermissionsAsync();
// If the user hasn't set a preference yet, ask them.
if (
!(
settings.status === 'undetermined' ||
settings.ios?.status === Notifications.IosAuthorizationStatus.NOT_DETERMINED
)
) {
settings = await Notifications.requestPermissionsAsync();
}
// If the user does not allow notifications, return null
if (
!(
settings.granted ||
settings.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL
)
) {
return null;
}
return (await Notifications.getExpoPushTokenAsync()).data;
}
return (await Notifications.getExpoPushTokenAsync()).data;
showMessage('Emulators will not recieve push notifications');
return null;
} catch (error) {
showMessage(
'Failed to get push token for push notification!',
'Error',
() => {},
true,
error
);
return null;
}
showMessage('Must use physical device for Push Notifications');
return undefined;
};

/**
Expand All @@ -141,7 +159,74 @@ const App = () => {
return (
<>
<StatusBar backgroundColor="blue" barStyle="dark-content" />
<NavigationContainer>
<NavigationContainer
linking={
// From https://docs.expo.dev/versions/latest/sdk/notifications/#handling-push-notifications-with-react-navigation
{
prefixes: ['danceblue.org', 'https://www.danceblue.org', 'danceblue://'],
config: {
screens: {
Main: {
initialRouteName: 'Tab',
screens: {
Tab: {
screens: {
Home: 'redirect',
Scoreboard: 'redirect/team-rankings',
Team: 'redirect/my-team',
Store: 'redirect/dancebluetique',
},
},
Profile: 'redirect/app-profile',
Notifications: 'redirect/app-notifications',
},
},
DefaultRoute: '*',
},
},
async getInitialURL() {
// First, you may want to do the default deep link handling
// Check if app was opened from a deep link
let url = await Linking.getInitialURL();

if (url != null) {
return url;
}

// Handle URL from expo push notifications
const response = await Notifications.getLastNotificationResponseAsync();
url = response?.notification.request.content.data.url;

return url;
},
subscribe(listener) {
const onReceiveURL = ({ url }) => listener(url);

// Listen to incoming links from deep linking
const deepLinkSubscription = Linking.addEventListener('url', onReceiveURL);

// Listen to expo push notifications
const expoSubscription = Notifications.addNotificationResponseReceivedListener(
(response) => {
const { url } = response.notification.request.content.data;

// Any custom logic to see whether the URL needs to be handled
// ...

// Let React Navigation handle the URL
listener(url);
}
);

return () => {
// Clean up the event listeners
deepLinkSubscription.remove();
expoSubscription.remove();
};
},
}
}
>
<RootScreen />
</NavigationContainer>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/navigation/HeaderIcons.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const HeaderIcons = ({ navigation, color }) => (
},
]}
>
<TouchableOpacity onPress={() => navigation.navigate('Profile')}>
<TouchableOpacity onPress={() => navigation.navigate('Notifications')}>
<FontAwesome5
name="bell"
color={color}
Expand Down
22 changes: 5 additions & 17 deletions src/navigation/MainStackRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BlurView } from 'expo-blur';

// Import first-party dependencies
import { EventView } from '../screens/EventScreen';
import GenericWebviewScreen from '../screens/GenericWebviewScreen';
import NotificationScreen from '../screens/NotificationScreen';
import ProfileScreen from '../screens/ProfileScreen';
import TabBar from './TabBar';
import HeaderIcons from './HeaderIcons';
Expand All @@ -21,24 +21,12 @@ const MainStackRoot = () => (
})}
>
<MainStack.Screen name="Tab" options={{ headerShown: false }} component={TabBar} />
<MainStack.Screen name="Profile" component={ProfileScreen} options={{ headerRight: null }} />
<MainStack.Screen
name="FAQ"
component={GenericWebviewScreen}
initialParams={{
uri: 'https://www.danceblue.org/frequently-asked-questions/',
}}
/>
<MainStack.Screen
name="Donate"
component={GenericWebviewScreen}
initialParams={{ uri: 'https://danceblue.networkforgood.com' }}
/>
<MainStack.Screen
name="About"
component={GenericWebviewScreen}
initialParams={{ uri: 'https://www.danceblue.org/about/' }}
name="Notifications"
component={NotificationScreen}
options={{ headerRight: null }}
/>
<MainStack.Screen name="Profile" component={ProfileScreen} options={{ headerRight: null }} />
<MainStack.Screen
name="Event"
component={EventView}
Expand Down
6 changes: 6 additions & 0 deletions src/navigation/RootScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SplashLogin from '../screens/Modals/SplashLogin';
import MainStackRoot from './MainStackRoot';
import { showMessage } from '../common/AlertUtils';
import { firebaseAuth } from '../common/FirebaseApp';
import GenericWebviewScreen from '../screens/GenericWebviewScreen';

// All assets that should be preloaded:
const profileButtonImage = require('../../assets/more/Profile_Button.jpg');
Expand Down Expand Up @@ -63,6 +64,11 @@ const RootScreen = () => {
options={{ headerShown: false }}
/>
)}
<RootStack.Screen
name="DefaultRoute"
component={GenericWebviewScreen}
options={{ headerBackTitle: 'Back', headerTitle: 'DanceBlue' }}
/>
</RootStack.Navigator>
);
};
Expand Down
30 changes: 23 additions & 7 deletions src/screens/GenericWebviewScreen/GenericWebviewScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,33 @@ import { globalStyles } from '../../theme';
*/
const GenericWebviewScreen = ({ route }) => {
const [isWebpageLoading, setIsWebpageLoading] = useState(true);

let { uri } = route.params;
if (!uri && uri !== '') {
// If a uri is not given (and I don't mean blank) then send a 404 from DanceBlue's website
uri = 'https://www.danceblue.org/404/';
// Is this a default case from react navigation deep linking?
if (route.path) {
return (
<View style={globalStyles.genericView}>
<WebView
source={{ uri: `https://www.danceblue.org${route.path}` }}
onLoadEnd={() => setIsWebpageLoading(false)}
/>
</View>
);
}

// Is this component being rendered by a navigator?
if (route?.params?.uri) {
return (
<View style={globalStyles.genericView}>
<WebView source={route.params} onLoadEnd={() => setIsWebpageLoading(false)} />
</View>
);
}
// Fallback to 404
return (
<View style={globalStyles.genericView}>
{isWebpageLoading && <ActivityIndicator />}
<WebView source={{ uri }} onLoadEnd={() => setIsWebpageLoading(false)} />
<WebView
source={{ uri: 'https://www.danceblue.org/404/' }}
onLoadEnd={() => setIsWebpageLoading(false)}
/>
</View>
);
};
Expand Down
59 changes: 59 additions & 0 deletions src/screens/NotificationScreen/NotificationScreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, Text, View } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { doc, getDoc } from 'firebase/firestore';
import { globalStyles, globalTextStyles } from '../../theme';
import { firebaseFirestore } from '../../common/FirebaseApp';
import { showMessage } from '../../common/AlertUtils';

const secureStoreKey = 'danceblue-device-uuid';
const secureStoreOptions = { keychainAccessible: SecureStore.ALWAYS_THIS_DEVICE_ONLY };

/**
* Component for "Profile" screen in main navigation
*/
const NotificationScreen = () => {
const [notifications, setNotifications] = useState([]);
const [notificationsListView, setNotificationsListView] = useState();

useEffect(() => {
SecureStore.isAvailableAsync().then(async (isAvailable) => {
if (isAvailable) {
await SecureStore.getItemAsync(secureStoreKey, secureStoreOptions).then(async (uuid) => {
// We have already set a UUID and can use the retrieved value
if (uuid) {
// Get this device's doc
const deviceRef = doc(firebaseFirestore, 'devices', uuid);
// Set notifications to the array found in firebase
getDoc(deviceRef).then((snapshot) =>
setNotifications(snapshot.get('pastNotifications'))
);
}
});
} else {
showMessage('Cannot retrieve device ID, push notificatons unavailable');
}
});
}, []);

useEffect(() => {
const tempNotificationsListView = [];
for (let i = 0; i < notifications.length; i++) {
tempNotificationsListView.push(
<View style={globalStyles.genericRow} key={i}>
<View style={globalStyles.genericView}>
<Text style={globalTextStyles.headerText}>{notifications[i].title}</Text>
<Text>{notifications[i].body}</Text>
</View>
</View>
);
}
setNotificationsListView(tempNotificationsListView);
}, [notifications]);

return (
<View style={globalStyles.genericView}>{notificationsListView || <ActivityIndicator />}</View>
);
};

export default NotificationScreen;
3 changes: 3 additions & 0 deletions src/screens/NotificationScreen/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import NotificationScreen from './NotificationScreen';

export default NotificationScreen;
9 changes: 8 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4214,6 +4214,13 @@ expo-constants@~13.0.0:
"@expo/config" "^6.0.6"
uuid "^3.3.2"

expo-device@~4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-4.1.0.tgz#ae31c5fdb526f1b8cd837403f783f837fb5ca42a"
integrity sha512-Eflp5XQZP5UD7UAK+euuQsLl1k7iX/CijSpmjvURp2+GkIrqcwv+sehTxDId1w+SmGHYFnu40vtldUtkCmEXdQ==
dependencies:
ua-parser-js "^0.7.19"

expo-error-recovery@~3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/expo-error-recovery/-/expo-error-recovery-3.0.4.tgz#de85c8c6b387d9b1e532256600882f2c2704383a"
Expand Down Expand Up @@ -8788,7 +8795,7 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"

ua-parser-js@^0.7.30:
ua-parser-js@^0.7.19, ua-parser-js@^0.7.30:
version "0.7.31"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
Expand Down

0 comments on commit 18c2822

Please sign in to comment.