From 14725e180ae199a8dcdc95fdc9d5a62611592b5c Mon Sep 17 00:00:00 2001 From: Ksan Date: Thu, 11 Jun 2026 01:29:48 +0200 Subject: [PATCH] push notifications not working for some reason --- .../config/SecurityConfig.java | 2 +- .../service/NotificationService.java | 4 +- frontend/components/UpdatePrompt.tsx | 138 +++++++++++++++++ frontend/config/gitea.tsx | 10 ++ frontend/hooks/usePushNotifications.tsx | 46 ++++++ frontend/hooks/useUpdatecheck.tsx | 137 +++++++++++++++++ frontend/index.tsx | 141 ++++++++++-------- frontend/package-lock.json | 10 ++ frontend/package.json | 1 + frontend/version.json | 3 + 10 files changed, 429 insertions(+), 63 deletions(-) create mode 100644 frontend/components/UpdatePrompt.tsx create mode 100644 frontend/config/gitea.tsx create mode 100644 frontend/hooks/usePushNotifications.tsx create mode 100644 frontend/hooks/useUpdatecheck.tsx create mode 100644 frontend/version.json diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/config/SecurityConfig.java b/backend/src/main/java/dev/ksan/etfoglasiserver/config/SecurityConfig.java index 1cb8aeb..76844fd 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/config/SecurityConfig.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/config/SecurityConfig.java @@ -56,7 +56,7 @@ public class SecurityConfig { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( - "http://localhost:3000", + "http://localhost:8081", "https://etf-oglasi.ksan.dev" )); diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/service/NotificationService.java b/backend/src/main/java/dev/ksan/etfoglasiserver/service/NotificationService.java index d8809cb..2715cf4 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/service/NotificationService.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/service/NotificationService.java @@ -31,9 +31,11 @@ public class NotificationService { .build(); try { - firebaseMessaging.send(message); + String id = firebaseMessaging.send(message); + System.out.println("Send:" + id); } catch (FirebaseMessagingException ex) { + ex.printStackTrace(); if (ex.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED) { deviceTokenRepo.delete(token); diff --git a/frontend/components/UpdatePrompt.tsx b/frontend/components/UpdatePrompt.tsx new file mode 100644 index 0000000..1305c5c --- /dev/null +++ b/frontend/components/UpdatePrompt.tsx @@ -0,0 +1,138 @@ +// components/UpdatePrompt.tsx +// ───────────────────────────────────────────────────────────────────────────── +// Modal that appears when a newer build is available. +// "Update now" opens the Gitea release page in the browser. +// "Later" dismisses it for the current session (it reappears next launch). +// ───────────────────────────────────────────────────────────────────────────── + +import React from 'react'; +import { + Linking, + Modal, + Pressable, + StyleSheet, + Text, + View, +} from 'react-native'; +import type { UpdateInfo } from '../hooks/useUpdatecheck'; +import { version as currentVersion } from '../version.json'; + +interface Props { + updateInfo: UpdateInfo; + onDismiss: () => void; +} + +export function UpdatePrompt({ updateInfo, onDismiss }: Props) { + function openRelease() { + Linking.openURL(updateInfo.releaseUrl).catch(() => { + // If the device can't open the URL, just dismiss so the app isn't stuck. + onDismiss(); + }); + } + + return ( + + + + Update available + + + A new version of the app is available.{'\n'} + You are on build {currentVersion}, + the latest is build{' '} + {updateInfo.latestVersion}. + + + + + Later + + + + Update now + + + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + card: { + width: '100%', + maxWidth: 380, + backgroundColor: '#fff', + borderRadius: 12, + padding: 24, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 8, + }, + title: { + fontSize: 18, + fontWeight: '700', + color: '#111', + marginBottom: 12, + }, + body: { + fontSize: 15, + color: '#444', + lineHeight: 22, + marginBottom: 24, + }, + bold: { + fontWeight: '700', + color: '#111', + }, + actions: { + flexDirection: 'row', + justifyContent: 'flex-end', + gap: 12, + }, + btn: { + paddingVertical: 10, + paddingHorizontal: 20, + borderRadius: 8, + }, + btnPrimary: { + backgroundColor: '#2563EB', + }, + btnPrimaryText: { + color: '#fff', + fontWeight: '600', + fontSize: 15, + }, + btnSecondary: { + backgroundColor: '#F3F4F6', + }, + btnSecondaryText: { + color: '#374151', + fontWeight: '600', + fontSize: 15, + }, +}); \ No newline at end of file diff --git a/frontend/config/gitea.tsx b/frontend/config/gitea.tsx new file mode 100644 index 0000000..ef56ecc --- /dev/null +++ b/frontend/config/gitea.tsx @@ -0,0 +1,10 @@ +export const GITEA_URL = 'https://git.ksan.dev'; + +export const GITEA_OWNER = 'ksan'; + +export const GITEA_REPO = 'etf-oglasi'; + + +export const RELEASE_TAG_PREFIX = 'mobile-v'; + +export const UPDATE_CHECK_TIMEOUT_MS = 8_000; diff --git a/frontend/hooks/usePushNotifications.tsx b/frontend/hooks/usePushNotifications.tsx new file mode 100644 index 0000000..43415f3 --- /dev/null +++ b/frontend/hooks/usePushNotifications.tsx @@ -0,0 +1,46 @@ + +import { useEffect } from "react"; +import messaging from "@react-native-firebase/messaging"; +import notifee, { AndroidImportance, EventType } from "@notifee/react-native"; + + +async function displayLocalNotification(title?: string, body?: string) { + if (!title && !body) return; + + const channelId = await notifee.createChannel({ + id: "default", + name: "General", + importance: AndroidImportance.HIGH, + }); + + await notifee.displayNotification({ + title, + body, + android: { + channelId, + pressAction: { id: "default" }, + }, + }); +} + + +export function usePushNotifications() { + useEffect(() => { + const unsubscribeMessage = messaging().onMessage(async remoteMessage => { + const title = remoteMessage.data?.title as string | undefined; + const body = remoteMessage.data?.body as string | undefined; + await displayLocalNotification(title, body); + }); + + const unsubscribeNotifee = notifee.onForegroundEvent(({ type, detail }) => { + if (type === EventType.PRESS) { + console.log("Notification tapped:", detail.notification); + } + }); + + return () => { + unsubscribeMessage(); + unsubscribeNotifee(); + }; + }, []); +} diff --git a/frontend/hooks/useUpdatecheck.tsx b/frontend/hooks/useUpdatecheck.tsx new file mode 100644 index 0000000..72b4147 --- /dev/null +++ b/frontend/hooks/useUpdatecheck.tsx @@ -0,0 +1,137 @@ + +import { useEffect, useState } from 'react'; +import { + GITEA_URL, + GITEA_OWNER, + GITEA_REPO, + RELEASE_TAG_PREFIX, + UPDATE_CHECK_TIMEOUT_MS, +} from '../config/gitea'; +import { version as currentVersion } from '../version.json'; + +export interface UpdateInfo { + /** Build number of the latest release (same unit as currentVersion). */ + latestVersion: number; + /** Direct URL the user can open to read the release notes / download the APK. */ + releaseUrl: string; + /** Tag name as stored on Gitea, e.g. "mobile-v42". */ + tagName: string; +} + +export interface UseUpdateCheckResult { + /** True while the API call is in flight. */ + checking: boolean; + /** Populated once the check completes and a newer build exists. */ + updateInfo: UpdateInfo | null; + /** Non-null when the check failed (network error, timeout, bad response). */ + error: Error | null; + /** Re-run the check manually (e.g. from a "check again" button). */ + recheck: () => void; +} + +// ─── Gitea releases API ────────────────────────────────────────────────────── + +interface GiteaRelease { + id: number; + tag_name: string; + name: string; + html_url: string; + draft: boolean; + prerelease: boolean; +} + +async function fetchLatestRelease(): Promise { + const url = `${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/latest`; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), UPDATE_CHECK_TIMEOUT_MS); + + try { + const res = await fetch(url, { signal: controller.signal }); + + if (!res.ok) { + throw new Error(`Gitea API returned HTTP ${res.status}`); + } + + return (await res.json()) as GiteaRelease; + } finally { + clearTimeout(timer); + } +} + +/** Parse the integer build number out of a tag like "mobile-v42". Returns NaN if it can't. */ +function parseBuildNumber(tagName: string): number { + if (!tagName.startsWith(RELEASE_TAG_PREFIX)) return NaN; + return parseInt(tagName.slice(RELEASE_TAG_PREFIX.length), 10); +} + + +export function useUpdatecheck(): UseUpdateCheckResult { + const [checking, setChecking] = useState(false); + const [updateInfo, setUpdateInfo] = useState(null); + const [error, setError] = useState(null); + // A counter we bump to re-trigger the effect without changing the dep array shape. + const [trigger, setTrigger] = useState(0); + + useEffect(() => { + let cancelled = false; + + async function check() { + setChecking(true); + setError(null); + + try { + const release = await fetchLatestRelease(); + + if (cancelled) return; + + // Skip drafts and pre-releases. + if (release.draft || release.prerelease) { + setUpdateInfo(null); + return; + } + + const latestVersion = parseBuildNumber(release.tag_name); + + if (isNaN(latestVersion)) { + // The tag doesn't follow our scheme — ignore it silently. + setUpdateInfo(null); + return; + } + + if (latestVersion > currentVersion) { + setUpdateInfo({ + latestVersion, + releaseUrl: release.html_url, + tagName: release.tag_name, + }); + } else { + setUpdateInfo(null); + } + } catch (err) { + if (cancelled) return; + // Don't surface AbortError as a real error — it's just a timeout. + if (err instanceof Error && err.name === 'AbortError') { + setError(new Error('Update check timed out. Check your connection.')); + } else { + setError(err instanceof Error ? err : new Error(String(err))); + } + } finally { + if (!cancelled) setChecking(false); + } + } + + check(); + + return () => { + cancelled = true; + }; + }, [trigger]); + + return { + checking, + updateInfo, + error, + recheck: () => setTrigger(t => t + 1), + }; +} \ No newline at end of file diff --git a/frontend/index.tsx b/frontend/index.tsx index c373b3d..e863d41 100644 --- a/frontend/index.tsx +++ b/frontend/index.tsx @@ -2,7 +2,7 @@ import "./globals.css"; -import React from "react"; +import React, {useState} from "react"; import { useColorScheme } from "react-native"; import { NavigationContainer, DefaultTheme, DarkTheme } from "@react-navigation/native"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; @@ -16,7 +16,11 @@ import {SafeAreaProvider} from "react-native-safe-area-context"; import AuthGate from "@/screens/AuthGate"; import {AuthProvider, useAuth} from "@/context/AuthContext"; import messaging, {setBackgroundMessageHandler} from "@react-native-firebase/messaging"; -import {getMessaging} from "@firebase/messaging"; + +import { useUpdatecheck } from "./hooks/useUpdatecheck"; +import { UpdatePrompt } from './components/UpdatePrompt'; +import { usePushNotifications } from "./hooks/usePushNotifications"; + messaging().setBackgroundMessageHandler(async () => {}); // Must be called before any navigator renders enableScreens(); @@ -45,6 +49,7 @@ function ProfileTab() { return user ? : ; } + export default function App() { const scheme = useColorScheme(); const dark = scheme === "dark"; @@ -54,72 +59,86 @@ export default function App() { ? { ...DarkTheme, colors: { ...DarkTheme.colors, background: "#0E0D0C" } } : { ...DefaultTheme, colors: { ...DefaultTheme.colors, background: "#FDFAF5" } }; + const { updateInfo } = useUpdatecheck(); + const [updateDismissed, setUpdateDismissed] = useState(false); + + + usePushNotifications(); + return ( - - - ({ - headerShown: false, + + + ({ + headerShown: false, - tabBarStyle: { - backgroundColor: tab.bg, - borderTopColor: tab.border, - borderTopWidth: 1, - height: 60, - paddingBottom: 8, - paddingTop: 6, - }, + tabBarStyle: { + backgroundColor: tab.bg, + borderTopColor: tab.border, + borderTopWidth: 1, + height: 60, + paddingBottom: 8, + paddingTop: 6, + }, - tabBarActiveTintColor: tab.active, - tabBarInactiveTintColor: tab.inactive, + tabBarActiveTintColor: tab.active, + tabBarInactiveTintColor: tab.inactive, - tabBarLabelStyle: { - fontSize: 10, - fontWeight: "600", - letterSpacing: 0.3, - }, + tabBarLabelStyle: { + fontSize: 10, + fontWeight: "600", + letterSpacing: 0.3, + }, - tabBarIcon: ({ focused, color, size }) => { - const icons: Record< - string, - { active: keyof typeof Ionicons.glyphMap; inactive: keyof typeof Ionicons.glyphMap } - > = { - "Subscribed": { active: "bookmark", inactive: "bookmark-outline" }, - "Discover": { active: "compass", inactive: "compass-outline" }, - "Profile": { active: "person-circle", inactive: "person-circle-outline" }, - }; - const set = icons[route.name]; + tabBarIcon: ({ focused, color, size }) => { + const icons: Record< + string, + { active: keyof typeof Ionicons.glyphMap; inactive: keyof typeof Ionicons.glyphMap } + > = { + "Subscribed": { active: "bookmark", inactive: "bookmark-outline" }, + "Discover": { active: "compass", inactive: "compass-outline" }, + "Profile": { active: "person-circle", inactive: "person-circle-outline" }, + }; + const set = icons[route.name]; - return ( - - ); - }, - })} - > - + ); + }, + })} + > + + + + + + + {/* Overlays the entire app, including the nav bar */} + {updateInfo && !updateDismissed && ( + setUpdateDismissed(true)} /> - - - - - + )} - + + ); -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 512fa3c..cf93d01 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^15.0.3", + "@notifee/react-native": "^9.1.8", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-firebase/app": "^24.0.0", "@react-native-firebase/messaging": "^24.0.0", @@ -3583,6 +3584,15 @@ "node": ">=12.4.0" } }, + "node_modules/@notifee/react-native": { + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@notifee/react-native/-/react-native-9.1.8.tgz", + "integrity": "sha512-Az/dueoPerJsbbjRxu8a558wKY+gONUrfoy3Hs++5OqbeMsR0dYe6P+4oN6twrLFyzAhEA1tEoZRvQTFDRmvQg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index c3e52cf..1bc0660 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.3", + "@notifee/react-native": "^9.1.8", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-firebase/app": "^24.0.0", "@react-native-firebase/messaging": "^24.0.0", diff --git a/frontend/version.json b/frontend/version.json new file mode 100644 index 0000000..0e33e3f --- /dev/null +++ b/frontend/version.json @@ -0,0 +1,3 @@ +{ + "version": 0 +} \ No newline at end of file