Add notifications support.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ yarn-error.log
|
|||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.apk
|
*.apk
|
||||||
.expo
|
.expo
|
||||||
|
*.app
|
||||||
|
|
||||||
android/**
|
android/**
|
||||||
ios/**
|
ios/**
|
||||||
3
app.json
3
app.json
@@ -88,7 +88,8 @@
|
|||||||
],
|
],
|
||||||
"expo-localization",
|
"expo-localization",
|
||||||
"expo-font",
|
"expo-font",
|
||||||
"expo-router"
|
"expo-router",
|
||||||
|
"expo-notifications"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
|
import * as Notifications from "expo-notifications";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
@@ -25,6 +26,7 @@ import { client } from "../graphql/client";
|
|||||||
import { useTheme as usePaperTheme } from "../hooks/useTheme";
|
import { useTheme as usePaperTheme } from "../hooks/useTheme";
|
||||||
import { persistor, store } from "../redux/store";
|
import { persistor, store } from "../redux/store";
|
||||||
import "../translations/i18n";
|
import "../translations/i18n";
|
||||||
|
import { registerForPushNotificationsAsync } from "../util/notificationHandler";
|
||||||
|
|
||||||
function AuthenticatedLayout() {
|
function AuthenticatedLayout() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -109,6 +111,33 @@ function AppContent({ currentUser, checkUserSession, bodyshop }: any) {
|
|||||||
checkUserSession();
|
checkUserSession();
|
||||||
}, [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) {
|
if (currentUser.authorized === null) {
|
||||||
return (
|
return (
|
||||||
<ThemedLayout>
|
<ThemedLayout>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Tabs } from "expo-router";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTheme } from "react-native-paper";
|
import { useTheme } from "react-native-paper";
|
||||||
|
|
||||||
function JobTabLayout(props) {
|
function JobTabLayout() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const theme = useTheme();
|
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 { 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 { t } = useTranslation();
|
||||||
|
const { jobId } = useLocalSearchParams();
|
||||||
|
console.log("*** ~ JobsStack ~ jobId:", jobId);
|
||||||
|
|
||||||
|
const handleUpload = useCallback(() => {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
openImagePicker(jobId);
|
||||||
|
}, [openImagePicker, jobId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
@@ -23,10 +41,18 @@ function JobsStack() {
|
|||||||
options={({ route }) => ({
|
options={({ route }) => ({
|
||||||
//headerShown: false,
|
//headerShown: false,
|
||||||
title: (route.params as any)?.title || "Job Details",
|
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>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default JobsStack;
|
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import { Chip, IconButton, Text, useTheme } from "react-native-paper";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
||||||
import { setCameraJobId } from "../../redux/app/app.actions";
|
|
||||||
import { openImagePicker } from "../../redux/photos/photos.actions";
|
import { openImagePicker } from "../../redux/photos/photos.actions";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setCameraJobId: (id) => dispatch(setCameraJobId(id)),
|
|
||||||
openImagePicker: (id) => dispatch(openImagePicker(id)),
|
openImagePicker: (id) => dispatch(openImagePicker(id)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function JobListComponent({ bodyshop }) {
|
|||||||
const jobs = data ? [...(data?.jobs || []), { id: "footer-spacer" }] : [];
|
const jobs = data ? [...(data?.jobs || []), { id: "footer-spacer" }] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, marginHorizontal: 12 }}>
|
<SafeAreaView style={{ flex: 1, marginHorizontal: 12 }} edges={["top"]}>
|
||||||
<Text
|
<Text
|
||||||
variant="headlineMedium"
|
variant="headlineMedium"
|
||||||
style={{ marginBottom: 12, fontWeight: "600" }}
|
style={{ marginBottom: 12, fontWeight: "600" }}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component";
|
import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component";
|
||||||
import { selectDeleteAfterUpload } from "@/redux/app/app.selectors";
|
import { selectDeleteAfterUpload } from "@/redux/app/app.selectors";
|
||||||
import { selectBodyshop, selectCurrentUser } from "@/redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "@/redux/user/user.selectors";
|
||||||
|
import { registerForPushNotificationsAsync } from "@/util/notificationHandler";
|
||||||
import { formatBytes } from "@/util/uploadUtils";
|
import { formatBytes } from "@/util/uploadUtils";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import * as Application from "expo-application";
|
import * as Application from "expo-application";
|
||||||
import Constants from "expo-constants";
|
import Constants from "expo-constants";
|
||||||
|
import * as Notifications from "expo-notifications";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert, ScrollView, StyleSheet, View } from "react-native";
|
import { Alert, ScrollView, StyleSheet, View } from "react-native";
|
||||||
import { Button, Card, Divider, List, Text } from "react-native-paper";
|
import { Button, Card, Divider, List, Text } from "react-native-paper";
|
||||||
@@ -107,7 +109,55 @@ function Tab({ bodyshop, currentUser }) {
|
|||||||
</View>
|
</View>
|
||||||
</List.Section>
|
</List.Section>
|
||||||
</Card.Content>
|
</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>
|
||||||
|
|
||||||
<Card style={styles.section}>
|
<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-application": "~7.0.7",
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
"expo-dev-client": "~6.0.16",
|
"expo-dev-client": "~6.0.16",
|
||||||
|
"expo-device": "^8.0.9",
|
||||||
"expo-file-system": "~19.0.17",
|
"expo-file-system": "~19.0.17",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.7",
|
||||||
@@ -8436,6 +8437,18 @@
|
|||||||
"expo": "*"
|
"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": {
|
"node_modules/expo-eas-client": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-1.0.7.tgz",
|
||||||
@@ -15282,6 +15295,32 @@
|
|||||||
"typescript-compare": "^0.0.2"
|
"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": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"expo-application": "~7.0.7",
|
"expo-application": "~7.0.7",
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
"expo-dev-client": "~6.0.16",
|
"expo-dev-client": "~6.0.16",
|
||||||
|
"expo-device": "^8.0.9",
|
||||||
"expo-file-system": "~19.0.17",
|
"expo-file-system": "~19.0.17",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
"expo-haptics": "~15.0.7",
|
"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