Add notifications support.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,7 @@ yarn-error.log
|
||||
*.tar.gz
|
||||
*.apk
|
||||
.expo
|
||||
*.app
|
||||
|
||||
android/**
|
||||
ios/**
|
||||
ios/**
|
||||
|
||||
3
app.json
3
app.json
@@ -88,7 +88,8 @@
|
||||
],
|
||||
"expo-localization",
|
||||
"expo-font",
|
||||
"expo-router"
|
||||
"expo-router",
|
||||
"expo-notifications"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DefaultTheme,
|
||||
ThemeProvider,
|
||||
} from "@react-navigation/native";
|
||||
import * as Notifications from "expo-notifications";
|
||||
import { Stack } from "expo-router";
|
||||
import {
|
||||
Icon,
|
||||
@@ -25,6 +26,7 @@ import { client } from "../graphql/client";
|
||||
import { useTheme as usePaperTheme } from "../hooks/useTheme";
|
||||
import { persistor, store } from "../redux/store";
|
||||
import "../translations/i18n";
|
||||
import { registerForPushNotificationsAsync } from "../util/notificationHandler";
|
||||
|
||||
function AuthenticatedLayout() {
|
||||
const { t } = useTranslation();
|
||||
@@ -109,6 +111,33 @@ function AppContent({ currentUser, checkUserSession, bodyshop }: any) {
|
||||
checkUserSession();
|
||||
}, [checkUserSession]);
|
||||
|
||||
useEffect(() => {
|
||||
registerForPushNotificationsAsync()
|
||||
.then((token) => console.log("Expo Push Token:", token))
|
||||
.catch((error: any) =>
|
||||
console.log("Error getting Expo Push Token:", error)
|
||||
);
|
||||
|
||||
const notificationListener = Notifications.addNotificationReceivedListener(
|
||||
async (notification) => {
|
||||
console.log("Notification received:", notification);
|
||||
Notifications.setBadgeCountAsync(
|
||||
(await Notifications.getBadgeCountAsync()) + 1
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const responseListener =
|
||||
Notifications.addNotificationResponseReceivedListener((response) => {
|
||||
console.log("Notification response received:", response);
|
||||
});
|
||||
|
||||
return () => {
|
||||
notificationListener.remove();
|
||||
responseListener.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (currentUser.authorized === null) {
|
||||
return (
|
||||
<ThemedLayout>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Tabs } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "react-native-paper";
|
||||
|
||||
function JobTabLayout(props) {
|
||||
function JobTabLayout() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { openImagePicker } from "@/redux/photos/photos.actions";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconButton } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
function JobsStack() {
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openImagePicker: (id) => dispatch(openImagePicker(id)),
|
||||
});
|
||||
export default connect(null, mapDispatchToProps)(JobsStack);
|
||||
|
||||
function JobsStack({ openImagePicker }) {
|
||||
const { t } = useTranslation();
|
||||
const { jobId } = useLocalSearchParams();
|
||||
console.log("*** ~ JobsStack ~ jobId:", jobId);
|
||||
|
||||
const handleUpload = useCallback(() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
openImagePicker(jobId);
|
||||
}, [openImagePicker, jobId]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
@@ -23,10 +41,18 @@ function JobsStack() {
|
||||
options={({ route }) => ({
|
||||
//headerShown: false,
|
||||
title: (route.params as any)?.title || "Job Details",
|
||||
headerRight: () => (
|
||||
<IconButton
|
||||
onPress={handleUpload}
|
||||
icon="cloud-upload-outline"
|
||||
mode="contained-tonal"
|
||||
size={8}
|
||||
//style={{ marginBottom: 1 }}
|
||||
accessibilityLabel={t("joblist.actions.upload")}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobsStack;
|
||||
|
||||
@@ -7,12 +7,10 @@ import { Chip, IconButton, Text, useTheme } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
||||
import { setCameraJobId } from "../../redux/app/app.actions";
|
||||
import { openImagePicker } from "../../redux/photos/photos.actions";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setCameraJobId: (id) => dispatch(setCameraJobId(id)),
|
||||
openImagePicker: (id) => dispatch(openImagePicker(id)),
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function JobListComponent({ bodyshop }) {
|
||||
const jobs = data ? [...(data?.jobs || []), { id: "footer-spacer" }] : [];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, marginHorizontal: 12 }}>
|
||||
<SafeAreaView style={{ flex: 1, marginHorizontal: 12 }} edges={["top"]}>
|
||||
<Text
|
||||
variant="headlineMedium"
|
||||
style={{ marginBottom: 12, fontWeight: "600" }}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component";
|
||||
import { selectDeleteAfterUpload } from "@/redux/app/app.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "@/redux/user/user.selectors";
|
||||
import { registerForPushNotificationsAsync } from "@/util/notificationHandler";
|
||||
import { formatBytes } from "@/util/uploadUtils";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as Application from "expo-application";
|
||||
import Constants from "expo-constants";
|
||||
import * as Notifications from "expo-notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert, ScrollView, StyleSheet, View } from "react-native";
|
||||
import { Button, Card, Divider, List, Text } from "react-native-paper";
|
||||
@@ -107,7 +109,55 @@ function Tab({ bodyshop, currentUser }) {
|
||||
</View>
|
||||
</List.Section>
|
||||
</Card.Content>
|
||||
<Card.Actions></Card.Actions>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
try {
|
||||
await registerForPushNotificationsAsync();
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
"Error",
|
||||
`Unable to register for notifications: ${error.message}`
|
||||
);
|
||||
console.log("Notification registration error:", error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Enable Notifications
|
||||
</Button>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
const projectId =
|
||||
Constants?.expoConfig?.extra?.eas?.projectId ??
|
||||
Constants?.easConfig?.projectId;
|
||||
|
||||
const pushTokenString = (
|
||||
await Notifications.getExpoPushTokenAsync({
|
||||
projectId,
|
||||
})
|
||||
).data;
|
||||
const message = {
|
||||
to: pushTokenString,
|
||||
sound: "default",
|
||||
title: "Original Title",
|
||||
body: "And here is the body!",
|
||||
data: { someData: "goes here" },
|
||||
};
|
||||
|
||||
await fetch("https://exp.host/--/api/v2/push/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Accept-encoding": "gzip, deflate",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Noti
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
|
||||
<Card style={styles.section}>
|
||||
|
||||
@@ -23,3 +23,11 @@ export const QUERY_SHOP_ID = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
export const UPDATE_FCM_TOKEN = gql`
|
||||
mutation UPDATE_FCM_TOKEN($email: String!, $token: jsonb!) {
|
||||
update_users(where: {email: {_eq: $email}}, _append: {fcmtokens: $token}) {
|
||||
affected_rows
|
||||
}
|
||||
}`;
|
||||
39
package-lock.json
generated
39
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"expo-application": "~7.0.7",
|
||||
"expo-constants": "~18.0.10",
|
||||
"expo-dev-client": "~6.0.16",
|
||||
"expo-device": "^8.0.9",
|
||||
"expo-file-system": "~19.0.17",
|
||||
"expo-font": "~14.0.9",
|
||||
"expo-haptics": "~15.0.7",
|
||||
@@ -8436,6 +8437,18 @@
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-device": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.9.tgz",
|
||||
"integrity": "sha512-XqRpaljDNAYZGZzMpC+b9KZfzfydtkwx3pJAp6ODDH+O/5wjAw+mLc5wQMGJCx8/aqVmMsAokec7iebxDPFZDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ua-parser-js": "^0.7.33"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-eas-client": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-1.0.7.tgz",
|
||||
@@ -15282,6 +15295,32 @@
|
||||
"typescript-compare": "^0.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "0.7.41",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
|
||||
"integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ua-parser-js"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/faisalman"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/faisalman"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"ua-parser-js": "script/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"expo-application": "~7.0.7",
|
||||
"expo-constants": "~18.0.10",
|
||||
"expo-dev-client": "~6.0.16",
|
||||
"expo-device": "^8.0.9",
|
||||
"expo-file-system": "~19.0.17",
|
||||
"expo-font": "~14.0.9",
|
||||
"expo-haptics": "~15.0.7",
|
||||
|
||||
72
util/notificationHandler.js
Normal file
72
util/notificationHandler.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { getCurrentUser } from '@/firebase/firebase.utils';
|
||||
import { UPDATE_FCM_TOKEN } from '@/graphql/bodyshop.queries';
|
||||
import { client } from '@/graphql/client';
|
||||
import Constants from 'expo-constants';
|
||||
import * as Device from 'expo-device';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
async function registerForPushNotificationsAsync() {
|
||||
console.log("Registering for push notifications...");
|
||||
if (Platform.OS === 'android') {
|
||||
await Notifications.setNotificationChannelAsync('default', {
|
||||
name: 'default',
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
vibrationPattern: [0, 250, 250, 250],
|
||||
lightColor: '#FF231F7C',
|
||||
});
|
||||
}
|
||||
|
||||
if (Device.isDevice) {
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||
let finalStatus = existingStatus;
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
if (finalStatus !== 'granted') {
|
||||
throw new Error('Permission not granted to get push token for push notification!');
|
||||
}
|
||||
const projectId =
|
||||
Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId;
|
||||
if (!projectId) {
|
||||
throw new Error('Project ID not found');
|
||||
}
|
||||
try {
|
||||
const pushTokenString = (
|
||||
await Notifications.getExpoPushTokenAsync({
|
||||
projectId,
|
||||
})
|
||||
).data;
|
||||
console.log(pushTokenString);
|
||||
|
||||
//Write the FCM token to database.
|
||||
|
||||
const result = await client.mutate({
|
||||
mutation: UPDATE_FCM_TOKEN,
|
||||
variables: {
|
||||
email: (await getCurrentUser()).email,
|
||||
token: { [pushTokenString]: { pushTokenString, platform: Platform.OS, timestamp: Date.now() } },
|
||||
},
|
||||
});
|
||||
console.log("FCM token updated in database:", result);
|
||||
return pushTokenString;
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
throw new Error('Must use physical device for push notifications');
|
||||
}
|
||||
}
|
||||
|
||||
export { registerForPushNotificationsAsync };
|
||||
Reference in New Issue
Block a user