From 6ead35f96d0cc9b128d6cc7042a9414b04cd7126 Mon Sep 17 00:00:00 2001 From: AntoKeinanen Date: Thu, 11 Jul 2024 16:33:05 +0300 Subject: [PATCH 1/7] fix: correctly remove event from favorites --- hooks/useFavorite.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts index c514584..c205567 100644 --- a/hooks/useFavorite.ts +++ b/hooks/useFavorite.ts @@ -48,10 +48,10 @@ export const useFavorite = (id: number) => { const toggle = async () => { let favorites = await getFavorites(); - if (favorite) { + if (!favorite) { favorites.push(id); } else { - favorites.filter((f) => f !== id); + favorites = favorites.filter((f) => f !== id); } saveFavorites(favorites); From 2c9436065d71a18bdf30991d3da218e3c8386d18 Mon Sep 17 00:00:00 2001 From: AntoKeinanen Date: Thu, 11 Jul 2024 16:34:55 +0300 Subject: [PATCH 2/7] feat: add favorite event notification 15 minutes before event starts --- app.json | 13 ++- app/_layout.tsx | 9 ++ components/timetable/Event.tsx | 2 +- hooks/useFavorite.ts | 6 +- hooks/useLocalNotification.ts | 108 ++++++++++++++++++++++++ package-lock.json | 146 +++++++++++++++++++++++++++++++++ package.json | 4 +- 7 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 hooks/useLocalNotification.ts diff --git a/app.json b/app.json index 05ebe89..5e45101 100644 --- a/app.json +++ b/app.json @@ -27,7 +27,18 @@ "output": "static", "favicon": "./assets/images/favicon.png" }, - "plugins": ["expo-router", "expo-build-properties"], + "plugins": [ + "expo-router", + "expo-build-properties", + [ + "expo-notifications", + { + "icon": "./assets/images/icon.png", + "color": "#191919", + "sounds": [] + } + ] + ], "experiments": { "typedRoutes": true }, diff --git a/app/_layout.tsx b/app/_layout.tsx index a7bbf71..358c8f9 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,6 +1,7 @@ import Locales from '@/locales'; import { Themes } from '@/styles'; import { useFonts } from 'expo-font'; +import * as Notifications from 'expo-notifications'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import i18n from 'i18next'; @@ -25,6 +26,14 @@ i18n.use(initReactI18next).init({ }, }); +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: true, + shouldSetBadge: false, + }), +}); + export default function RootLayout() { const [loaded] = useFonts({ Gaba: require('../assets/fonts/Gaba-Super.otf'), diff --git a/components/timetable/Event.tsx b/components/timetable/Event.tsx index 67eeebb..24bd910 100644 --- a/components/timetable/Event.tsx +++ b/components/timetable/Event.tsx @@ -27,7 +27,7 @@ const getEventTimeString = (start: Date, end: Date) => { const Event = ({ id, title, location, start, end, color, thumbnail }: EventProps) => { const timeString = getEventTimeString(start, end); - const { favorite, toggle: toggleFavorite } = useFavorite(id); + const { favorite, toggle: toggleFavorite } = useFavorite(id, title, start); const { t, i18n } = useTranslation(); dayjs.locale(i18n.language); diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts index c205567..04ff90b 100644 --- a/hooks/useFavorite.ts +++ b/hooks/useFavorite.ts @@ -1,3 +1,4 @@ +import { schedulePushNotification, useLocalNotification } from './useLocalNotification'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useEffect, useState } from 'react'; @@ -34,10 +35,12 @@ const saveFavorites = async (favorites: number[]) => { * Hook to manage favorite state for a given eventId. * * @param id - The ID of the event to track favorite state for. + * @param eventTitle - The title of the event. * @returns An object containing the current favorite state and a function to toggle the favorite state. */ -export const useFavorite = (id: number) => { +export const useFavorite = (id: number, eventTitle: string, start: Date) => { const [favorite, setFavorite] = useState(false); + useLocalNotification(); useEffect(() => { getFavorites().then((favorites) => { @@ -50,6 +53,7 @@ export const useFavorite = (id: number) => { if (!favorite) { favorites.push(id); + schedulePushNotification(eventTitle, start); } else { favorites = favorites.filter((f) => f !== id); } diff --git a/hooks/useLocalNotification.ts b/hooks/useLocalNotification.ts new file mode 100644 index 0000000..b26451d --- /dev/null +++ b/hooks/useLocalNotification.ts @@ -0,0 +1,108 @@ +import dayjs from 'dayjs'; +import * as Device from 'expo-device'; +import * as Notifications from 'expo-notifications'; +import { useEffect, useRef, useState } from 'react'; +import { Platform } from 'react-native'; + +interface ILocalNotificationHook { + expoPushToken: string | undefined; + notification: Notifications.Notification; +} + +/** + * Custom hook for managing local notifications and ensuring permissions are correctly set. + * + * @returns An object containing the Expo push token and the current notification. + */ +export const useLocalNotification = (): ILocalNotificationHook => { + const [expoPushToken, setExpoPushToken] = useState(''); + const [notification, setNotification] = useState({} as Notifications.Notification); + const notificationListener = useRef(); + const responseListener = useRef(); + + useEffect(() => { + registerForPushNotificationsAsync().then((token) => { + setExpoPushToken(token || ''); + }); + + notificationListener.current = Notifications.addNotificationReceivedListener( + (notification) => { + setNotification(notification); + } + ); + + responseListener.current = Notifications.addNotificationResponseReceivedListener( + (response) => { + setNotification(response.notification); + } + ); + + return () => { + if (notificationListener.current?.remove) { + notificationListener.current.remove(); + } + if (responseListener.current?.remove) { + responseListener.current.remove(); + } + }; + }, []); + + return { expoPushToken, notification }; +}; + +/** + * Schedules a push notification for a given event. Notification is set 15 minutes before given start date. + * @param eventTitle - The title of the event. + * @param start - The start date of the event. + */ +export const schedulePushNotification = async (eventTitle: string, start: Date) => { + const quarter_before_start = dayjs(start).subtract(15, 'minutes'); + const time_difference = Math.round(dayjs(quarter_before_start).diff(new Date()) / 1000); + + await Notifications.scheduleNotificationAsync({ + identifier: eventTitle, + content: { + title: `${eventTitle}`, + subtitle: '', + body: 'The event is starting in 15 minutes', + }, + trigger: { + seconds: time_difference, + }, + }); +}; + +/** + * Registers the device for push notifications and returns the push token. + * @returns A promise that resolves to the push token. + */ +export const registerForPushNotificationsAsync = async () => { + let token: string = ''; + + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'default', + importance: Notifications.AndroidImportance.MAX, + vibrationPattern: [0, 250, 250, 250], + lightColor: '#FFAABBCC', + }); + } + + 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') { + alert('Failed to get push token for push notification!'); + return; + } + token = (await Notifications.getExpoPushTokenAsync()).data; + } else { + alert('Must use physical device for Push Notifications'); + } + + return token; +}; diff --git a/package-lock.json b/package-lock.json index 0ebbc2b..70a03c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,10 @@ "expo": "~51.0.14", "expo-build-properties": "~0.12.3", "expo-constants": "~16.0.2", + "expo-device": "~6.0.2", "expo-font": "~12.0.7", "expo-linking": "~6.3.1", + "expo-notifications": "~0.28.9", "expo-router": "~3.5.16", "expo-splash-screen": "~0.27.5", "expo-status-bar": "~1.12.1", @@ -3250,6 +3252,11 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ide/backoff": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", + "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "license": "ISC", @@ -7115,6 +7122,18 @@ "version": "2.0.6", "license": "MIT" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/ast-types": { "version": "0.15.2", "license": "MIT", @@ -7391,6 +7410,11 @@ "@babel/core": "^7.0.0" } }, + "node_modules/badgin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz", + "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==" + }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" @@ -9499,6 +9523,14 @@ "expo": "bin/cli" } }, + "node_modules/expo-application": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-5.9.1.tgz", + "integrity": "sha512-uAfLBNZNahnDZLRU41ZFmNSKtetHUT9Ua557/q189ua0AWV7pQjoVAx49E4953feuvqc9swtU3ScZ/hN1XO/FQ==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-asset": { "version": "10.0.9", "license": "MIT", @@ -9543,6 +9575,39 @@ "expo": "*" } }, + "node_modules/expo-device": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-6.0.2.tgz", + "integrity": "sha512-sCt91CuTmAuMXX4SlFOn4lIos2UIr8vb0jDstDDZXys6kErcj0uynC7bQAMreU5uRUTKMAl4MAMpKt9ufCXPBw==", + "dependencies": { + "ua-parser-js": "^0.7.33" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-device/node_modules/ua-parser-js": { + "version": "0.7.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", + "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", + "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" + } + ], + "engines": { + "node": "*" + } + }, "node_modules/expo-file-system": { "version": "17.0.1", "license": "MIT", @@ -9684,6 +9749,57 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-notifications": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.28.9.tgz", + "integrity": "sha512-lilBS+0n+MC71jU6R9kYr91nev2God/OyxGRtab37XBmr7OWKL+jrahwdIRc2fJXq3hkQoiVWr4i9COGVFF8sA==", + "dependencies": { + "@expo/image-utils": "^0.5.0", + "@ide/backoff": "^1.0.0", + "abort-controller": "^3.0.0", + "assert": "^2.0.0", + "badgin": "^1.1.5", + "expo-application": "~5.9.0", + "expo-constants": "~16.0.0", + "fs-extra": "^9.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-notifications/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/expo-notifications/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/expo-notifications/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/expo-router": { "version": "3.5.16", "license": "MIT", @@ -10995,6 +11111,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { "version": "2.0.3", "license": "MIT", @@ -14674,6 +14805,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "license": "MIT", diff --git a/package.json b/package.json index 0b992c6..6b0061e 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ "react-native-safe-area-context": "^4.10.1", "react-native-screens": "3.31.1", "react-native-web": "~0.19.10", - "@react-native-async-storage/async-storage": "1.23.1" + "@react-native-async-storage/async-storage": "1.23.1", + "expo-notifications": "~0.28.9", + "expo-device": "~6.0.2" }, "devDependencies": { "@babel/core": "^7.20.0", From ece2c5f4e392c3ac432a6b00b5144132ff3d33d3 Mon Sep 17 00:00:00 2001 From: AntoKeinanen Date: Thu, 11 Jul 2024 16:48:21 +0300 Subject: [PATCH 3/7] feat: add translations --- hooks/useLocalNotification.ts | 5 +++-- locales/en.ts | 1 + locales/fi.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/hooks/useLocalNotification.ts b/hooks/useLocalNotification.ts index b26451d..7e3104e 100644 --- a/hooks/useLocalNotification.ts +++ b/hooks/useLocalNotification.ts @@ -1,6 +1,7 @@ import dayjs from 'dayjs'; import * as Device from 'expo-device'; import * as Notifications from 'expo-notifications'; +import { t } from 'i18next'; import { useEffect, useRef, useState } from 'react'; import { Platform } from 'react-native'; @@ -64,10 +65,10 @@ export const schedulePushNotification = async (eventTitle: string, start: Date) content: { title: `${eventTitle}`, subtitle: '', - body: 'The event is starting in 15 minutes', + body: t('event-starting-15'), }, trigger: { - seconds: time_difference, + seconds: 1, }, }); }; diff --git a/locales/en.ts b/locales/en.ts index 40617b0..a9487ed 100644 --- a/locales/en.ts +++ b/locales/en.ts @@ -9,5 +9,6 @@ export default { english: 'English', 'success-lang-change': 'Language changed successfully', 'error-lang-change': 'Error changing language', + 'event-starting-15': 'Event is starting in 15 minutes', }, }; diff --git a/locales/fi.ts b/locales/fi.ts index 5ff7c34..c21efdc 100644 --- a/locales/fi.ts +++ b/locales/fi.ts @@ -9,5 +9,6 @@ export default { english: 'Englanti', 'success-lang-change': 'Kieli vaihdettu onnistuneesti', 'error-lang-change': 'Virhe kielen vaihdossa', + 'event-starting-15': 'Tapahtuma alkaa 15 minuutin kuluttua', }, }; From 5760fdc6a9a258633a1548ce3d122c094b90b504 Mon Sep 17 00:00:00 2001 From: AntoKeinanen Date: Thu, 11 Jul 2024 16:48:46 +0300 Subject: [PATCH 4/7] fix: remove hardcoded time --- hooks/useLocalNotification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/useLocalNotification.ts b/hooks/useLocalNotification.ts index 7e3104e..861d042 100644 --- a/hooks/useLocalNotification.ts +++ b/hooks/useLocalNotification.ts @@ -68,7 +68,7 @@ export const schedulePushNotification = async (eventTitle: string, start: Date) body: t('event-starting-15'), }, trigger: { - seconds: 1, + seconds: time_difference, }, }); }; From 2544770264076b97316ecef7453c9a5f138419c4 Mon Sep 17 00:00:00 2001 From: AntoKeinanen Date: Fri, 12 Jul 2024 10:43:31 +0300 Subject: [PATCH 5/7] feat: cancel event notification when event is removed from favorites --- hooks/useFavorite.ts | 7 ++++++- hooks/useLocalNotification.ts | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts index 04ff90b..0e3b38f 100644 --- a/hooks/useFavorite.ts +++ b/hooks/useFavorite.ts @@ -1,4 +1,8 @@ -import { schedulePushNotification, useLocalNotification } from './useLocalNotification'; +import { + cancelScheduledPushNotification, + schedulePushNotification, + useLocalNotification, +} from './useLocalNotification'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useEffect, useState } from 'react'; @@ -56,6 +60,7 @@ export const useFavorite = (id: number, eventTitle: string, start: Date) => { schedulePushNotification(eventTitle, start); } else { favorites = favorites.filter((f) => f !== id); + cancelScheduledPushNotification(eventTitle); } saveFavorites(favorites); diff --git a/hooks/useLocalNotification.ts b/hooks/useLocalNotification.ts index 861d042..e7592eb 100644 --- a/hooks/useLocalNotification.ts +++ b/hooks/useLocalNotification.ts @@ -73,6 +73,16 @@ export const schedulePushNotification = async (eventTitle: string, start: Date) }); }; +/** + * Cancels a scheduled push notification with the specified event title. + * + * @param eventTitle - The title of the event associated with the push notification. + * @returns A promise that resolves when the notification is successfully canceled. + */ +export const cancelScheduledPushNotification = async (eventTitle: string) => { + await Notifications.cancelScheduledNotificationAsync(eventTitle); +}; + /** * Registers the device for push notifications and returns the push token. * @returns A promise that resolves to the push token. From 4eb03c6e26ce84a0500f198e2a3d7ec174e7a78e Mon Sep 17 00:00:00 2001 From: Samu Kupiainen Date: Sat, 27 Jul 2024 13:32:52 +0300 Subject: [PATCH 6/7] fix: favorite toggle usage --- elements/timetable/EventsBox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elements/timetable/EventsBox.tsx b/elements/timetable/EventsBox.tsx index 5d632ae..d8970a3 100644 --- a/elements/timetable/EventsBox.tsx +++ b/elements/timetable/EventsBox.tsx @@ -5,7 +5,7 @@ import { ScrollView } from 'react-native'; interface EventsBoxProps { events: AssemblyEvent[]; favorites: number[]; - toggleFavorite: (id: number) => void; + toggleFavorite: (id: number, title: string, start: Date) => void; } const EventsBox = ({ events, favorites, toggleFavorite }: EventsBoxProps) => { @@ -24,7 +24,7 @@ const EventsBox = ({ events, favorites, toggleFavorite }: EventsBoxProps) => { end={event.end} color={event.color} thumbnail={event.thumbnail} - toggleFavorite={() => toggleFavorite(event.id)} + toggleFavorite={() => toggleFavorite(event.id, event.title, event.start)} isFavorite={favorites.includes(event.id)} /> ))} From d5d95595df7edc96511b96582c04d4ca32c719ba Mon Sep 17 00:00:00 2001 From: Samu Kupiainen Date: Sat, 27 Jul 2024 15:12:11 +0300 Subject: [PATCH 7/7] fix: finish notification functionality --- app.config.js | 18 +++++++++-- app.json | 55 ---------------------------------- components/timetable/Event.tsx | 26 ++++++++-------- hooks/useFavorite.ts | 10 ++----- hooks/useLocalNotification.ts | 14 ++++----- package.json | 1 + 6 files changed, 39 insertions(+), 85 deletions(-) delete mode 100644 app.json diff --git a/app.config.js b/app.config.js index 3805105..8873a7e 100644 --- a/app.config.js +++ b/app.config.js @@ -18,7 +18,7 @@ export default () => { bundleIdentifier: process.env.EXPO_PUBLIC_ENVIRONMENT === 'production' ? 'com.testausserveri.assemblyapp' - : 'com.testausserveri.assemblyapp-dev', + : 'com.testausserveri.assemblyapp_dev', }, android: { adaptiveIcon: { @@ -28,14 +28,26 @@ export default () => { package: process.env.EXPO_PUBLIC_ENVIRONMENT === 'production' ? 'com.testausserveri.assemblyapp' - : 'com.testausserveri.assemblyapp-dev', + : 'com.testausserveri.assemblyapp_dev', + useNextNotificationApi: true, }, web: { bundler: 'metro', output: 'static', favicon: './assets/images/favicon.png', }, - plugins: ['expo-router', 'expo-build-properties'], + plugins: [ + 'expo-router', + 'expo-build-properties', + [ + 'expo-notifications', + { + icon: './assets/images/icon.png', + color: '#191919', + sounds: [], + }, + ], + ], experiments: { typedRoutes: true, }, diff --git a/app.json b/app.json deleted file mode 100644 index 7917b3b..0000000 --- a/app.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "expo": { - "name": "Assembly", - "slug": "assembly-app", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/images/icon.png", - "scheme": "myapp", - "userInterfaceStyle": "automatic", - "splash": { - "image": "./assets/images/splash.png", - "resizeMode": "contain", - "backgroundColor": "#191919" - }, - "ios": { - "supportsTablet": true - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive-icon.png", - "backgroundColor": "#191919" - }, - "package": "com.testausserveri.assemblyapp" - }, - "web": { - "bundler": "metro", - "output": "static", - "favicon": "./assets/images/favicon.png" - }, - "plugins": [ - "expo-router", - "expo-build-properties", - [ - "expo-notifications", - { - "icon": "./assets/images/icon.png", - "color": "#191919", - "sounds": [] - } - ] - ], - "experiments": { - "typedRoutes": true - }, - "extra": { - "router": { - "origin": false - }, - "eas": { - "projectId": "469c71b6-54c5-4111-bc4a-03e2cf92a23d" - } - }, - "owner": "testausserveri" - } -} diff --git a/components/timetable/Event.tsx b/components/timetable/Event.tsx index 0a03ca0..155fed7 100644 --- a/components/timetable/Event.tsx +++ b/components/timetable/Event.tsx @@ -110,19 +110,19 @@ const Event = ({ {`${t('time')}: ${timeString}`} - {dayjs().isBefore(start) || - ((process.env.EXPO_PUBLIC_ENVIRONMENT === 'development' || - process.env.EXPO_PUBLIC_ENVIRONMENT === 'preview') && ( - toggleFavorite()} - icon={isFavorite ? 'heart' : 'heart-outline'} - style={{ - position: 'absolute', - top: 0, - right: 0, - }} - /> - ))} + {(dayjs().isBefore(start) || + process.env.EXPO_PUBLIC_ENVIRONMENT === 'development' || + process.env.EXPO_PUBLIC_ENVIRONMENT === 'preview') && ( + toggleFavorite()} + icon={isFavorite ? 'heart' : 'heart-outline'} + style={{ + position: 'absolute', + top: 0, + right: 0, + }} + /> + )} ); }; diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts index a4b3507..60d3f3f 100644 --- a/hooks/useFavorite.ts +++ b/hooks/useFavorite.ts @@ -1,8 +1,4 @@ -import { - cancelScheduledPushNotification, - schedulePushNotification, - useLocalNotification, -} from './useLocalNotification'; +import { cancelScheduledPushNotification, schedulePushNotification } from './useLocalNotification'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useEffect, useState } from 'react'; @@ -53,11 +49,11 @@ export const useFavorite = () => { if (!favorites.includes(id)) { setFavorites([...favorites, id]); await saveFavorites([...favorites, id]); - schedulePushNotification(eventTitle, start); + schedulePushNotification(id, eventTitle, start); } else { setFavorites(favorites.filter((n) => n !== id)); await saveFavorites(favorites.filter((n) => n !== id)); - cancelScheduledPushNotification(eventTitle); + cancelScheduledPushNotification(id); } }; diff --git a/hooks/useLocalNotification.ts b/hooks/useLocalNotification.ts index e7592eb..2e43d60 100644 --- a/hooks/useLocalNotification.ts +++ b/hooks/useLocalNotification.ts @@ -56,19 +56,19 @@ export const useLocalNotification = (): ILocalNotificationHook => { * @param eventTitle - The title of the event. * @param start - The start date of the event. */ -export const schedulePushNotification = async (eventTitle: string, start: Date) => { - const quarter_before_start = dayjs(start).subtract(15, 'minutes'); - const time_difference = Math.round(dayjs(quarter_before_start).diff(new Date()) / 1000); +export const schedulePushNotification = async (id: number, eventTitle: string, start: Date) => { + const quarterBeforeStart = dayjs(start).subtract(15, 'minutes'); + const timeDifference = Math.round(dayjs(quarterBeforeStart).diff(new Date(), 'seconds')); await Notifications.scheduleNotificationAsync({ - identifier: eventTitle, + identifier: id.toString(), content: { title: `${eventTitle}`, subtitle: '', body: t('event-starting-15'), }, trigger: { - seconds: time_difference, + seconds: timeDifference, }, }); }; @@ -79,8 +79,8 @@ export const schedulePushNotification = async (eventTitle: string, start: Date) * @param eventTitle - The title of the event associated with the push notification. * @returns A promise that resolves when the notification is successfully canceled. */ -export const cancelScheduledPushNotification = async (eventTitle: string) => { - await Notifications.cancelScheduledNotificationAsync(eventTitle); +export const cancelScheduledPushNotification = async (id: number) => { + await Notifications.cancelScheduledNotificationAsync(id.toString()); }; /** diff --git a/package.json b/package.json index 20036b9..99e32d5 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "jest --watchAll", "lint": "expo lint", "pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"", + "build:android:preview": "eas build --platform android --profile preview --non-interactive --no-wait", "check:types": "tsc --noEmit", "check:lint": "expo lint", "check:format": "prettier --check \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"",