From df4e78332be6ff7d565cdaf83e5820c65d3a2672 Mon Sep 17 00:00:00 2001 From: Irfan Emre Utkan <127414322+emreutkan@users.noreply.github.com> Date: Fri, 23 May 2025 12:33:22 +0300 Subject: [PATCH 1/5] feat: update API base URL and enhance user session management with clearUserSession action --- src/components/AuthMiddleware.tsx | 48 +++++++++++-------------------- src/redux/Api/apiService.ts | 2 +- src/redux/slices/userSlice.ts | 12 +++++++- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/components/AuthMiddleware.tsx b/src/components/AuthMiddleware.tsx index a1bab8f..6bfae92 100644 --- a/src/components/AuthMiddleware.tsx +++ b/src/components/AuthMiddleware.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { AppDispatch, RootState } from '../redux/store'; import { getUserData } from '../redux/thunks/userThunks'; +import { logout } from '../redux/slices/userSlice'; // Simple debounce utility const useDebounce = (callback: Function, delay: number) => { @@ -28,7 +29,7 @@ const AuthMiddleware: React.FC = ({ children }) => { const { token, role, loading, error } = user; const [hasAttempted, setHasAttempted] = useState(false); const attemptCountRef = useRef(0); - const MAX_ATTEMPTS = 3; + const MAX_ATTEMPTS = 1; // Debug log whenever user state changes useEffect(() => { @@ -47,11 +48,24 @@ const AuthMiddleware: React.FC = ({ children }) => { attemptCountRef.current += 1; const response = await dispatch(getUserData()); console.log('Fetch response:', response); - const role = useSelector((state: RootState) => state.user.role); - console.log('Fetched role:', role); + // Don't use hooks here - removed the invalid hook call } }, 1000); + // Effect to check for max attempts and clear token if needed + useEffect(() => { + if ( + attemptCountRef.current >= MAX_ATTEMPTS && + error && + token && + error.includes("404") + ) { + console.log('Maximum attempts reached with 404 errors. Clearing invalid token.'); + dispatch(logout()); + // No need for window.location.reload() as the state change will trigger a re-render + } + }, [attemptCountRef.current, error, token, dispatch]); + useEffect(() => { // If we have a token but no role and haven't tried fetching yet if (token && !role && !loading && !hasAttempted) { @@ -74,34 +88,6 @@ const AuthMiddleware: React.FC = ({ children }) => { } }, [token, role, loading, hasAttempted, debouncedFetchUserData]); - // For extreme debugging, force-update the role from localStorage if available - useEffect(() => { - // This is a workaround if the reducer isn't working properly - const tryRestoreRoleFromLocalStorage = () => { - try { - // Only do this if we've exhausted our API attempts - if (attemptCountRef.current >= MAX_ATTEMPTS && token && !role) { - const savedUserData = localStorage.getItem('userData'); - if (savedUserData) { - const userData = JSON.parse(savedUserData); - if (userData.role) { - console.log('Restoring role from localStorage:', userData.role); - // You would need to add a setRole action to your userSlice - // dispatch(setRole(userData.role)); - window.location.reload(); // Extreme measure, reload the page - } - } - } - } catch (e) { - console.error('Error restoring role from localStorage:', e); - } - }; - - if (attemptCountRef.current >= MAX_ATTEMPTS) { - tryRestoreRoleFromLocalStorage(); - } - }, [attemptCountRef.current, token, role, dispatch]); - return <>{children}; }; diff --git a/src/redux/Api/apiService.ts b/src/redux/Api/apiService.ts index 8a1aaf8..7aabde5 100644 --- a/src/redux/Api/apiService.ts +++ b/src/redux/Api/apiService.ts @@ -1,7 +1,7 @@ import {logout} from "../slices/userSlice.ts"; // export const API_BASE_URL = 'https://freshdealbackend.azurewebsites.net/v1'; -export const API_BASE_URL = 'http://192.168.1.6:8000/v1'; +export const API_BASE_URL = 'http://192.168.1.3:8000/v1'; export const TOKEN_KEY = 'userToken'; diff --git a/src/redux/slices/userSlice.ts b/src/redux/slices/userSlice.ts index d7c7027..7af93f5 100644 --- a/src/redux/slices/userSlice.ts +++ b/src/redux/slices/userSlice.ts @@ -82,6 +82,15 @@ const userSlice = createSlice({ state.token = action.payload; setStoredToken(action.payload); }, + clearUserSession: (state) => { + state.token = null; + state.role = ''; + state.loading = false; + state.error = null; + // Clear any stored token + localStorage.removeItem('token'); + localStorage.removeItem('userData'); + }, logout() { removeStoredToken(); localStorage.removeItem('userData'); @@ -287,7 +296,8 @@ export const { setToken, setRole, logout, - restoreUserDataFromStorage + restoreUserDataFromStorage, + clearUserSession } = userSlice.actions; export default userSlice.reducer; \ No newline at end of file From efc04489ca0af63ae901c734e6b5fef2506b7b3b Mon Sep 17 00:00:00 2001 From: Irfan Emre Utkan <127414322+emreutkan@users.noreply.github.com> Date: Fri, 23 May 2025 14:44:32 +0300 Subject: [PATCH 2/5] feat: add notification button and modal for managing notification settings --- .../NotificationPermission.module.css | 70 -------- .../NotificationPermission.tsx | 0 src/feature/Header/Header.module.css | 27 +++- src/feature/Header/Header.tsx | 51 +++++- .../components/NotificationModal.module.css | 113 +++++++++++++ .../components/NotificationModal.tsx} | 150 ++++++++---------- .../NotificationTest.module.css | 57 ------- .../screens/RestaurantDetails.tsx | 2 - 8 files changed, 256 insertions(+), 214 deletions(-) delete mode 100644 src/components/NotificationPermission/NotificationPermission.module.css delete mode 100644 src/components/NotificationPermission/NotificationPermission.tsx create mode 100644 src/feature/Header/components/NotificationModal.module.css rename src/feature/{RestaurantDetails/components/NotificationTest/NotificationTest.tsx => Header/components/NotificationModal.tsx} (56%) delete mode 100644 src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.module.css diff --git a/src/components/NotificationPermission/NotificationPermission.module.css b/src/components/NotificationPermission/NotificationPermission.module.css deleted file mode 100644 index 145b6a1..0000000 --- a/src/components/NotificationPermission/NotificationPermission.module.css +++ /dev/null @@ -1,70 +0,0 @@ -.notificationPrompt { - position: fixed; - bottom: 20px; - right: 20px; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1000; - max-width: 400px; - width: 100%; - overflow: hidden; -} - -.promptContent { - padding: 16px; - display: flex; - flex-direction: column; - align-items: center; -} - -.bellIcon { - font-size: 48px; - color: #3b82f6; - margin-bottom: 16px; -} - -.promptText { - text-align: center; - margin-bottom: 16px; -} - -.promptText h3 { - margin: 0 0 8px 0; - font-size: 18px; - font-weight: 600; -} - -.promptText p { - margin: 0; - color: #6b7280; - font-size: 14px; -} - -.promptActions { - display: flex; - justify-content: center; - gap: 12px; - width: 100%; -} - -.allowButton { - background-color: #3b82f6 !important; - color: white !important; - padding: 8px 16px !important; - font-weight: 500 !important; -} - -.dismissButton { - color: #6b7280 !important; -} - -@media (max-width: 480px) { - .notificationPrompt { - bottom: 0; - right: 0; - width: 100%; - max-width: 100%; - border-radius: 8px 8px 0 0; - } -} \ No newline at end of file diff --git a/src/components/NotificationPermission/NotificationPermission.tsx b/src/components/NotificationPermission/NotificationPermission.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/feature/Header/Header.module.css b/src/feature/Header/Header.module.css index cd059e1..f479be2 100644 --- a/src/feature/Header/Header.module.css +++ b/src/feature/Header/Header.module.css @@ -99,4 +99,29 @@ text-overflow: ellipsis; white-space: nowrap; } -} \ No newline at end of file +} + +/* Add this to your existing Header.module.css */ + +.notificationButton { + background: none; + border: none; + margin-right: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: relative; + padding: 5px; +} + +.notificationIcon { + font-size: 24px; + color: #333; + transition: color 0.2s ease; +} + +.notificationButton:hover .notificationIcon { + color: #007bff; +} + diff --git a/src/feature/Header/Header.tsx b/src/feature/Header/Header.tsx index 86ff90d..9275085 100644 --- a/src/feature/Header/Header.tsx +++ b/src/feature/Header/Header.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; import styles from './Header.module.css'; @@ -6,11 +6,15 @@ import logo from '../../assets/fresh-deal-logo.svg'; import { RootState, AppDispatch } from '../../redux/store'; import { logout } from '../../redux/slices/userSlice'; import { IoLogOutOutline } from 'react-icons/io5'; +import { IoMdNotificationsOutline, IoMdNotifications } from 'react-icons/io'; +import NotificationModal from './components/NotificationModal'; const Header: React.FC = () => { const navigate = useNavigate(); const dispatch = useDispatch(); const { token, name_surname, role } = useSelector((state: RootState) => state.user); + const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false); + const [notificationStatus, setNotificationStatus] = useState(false); useEffect(() => { const token = localStorage.getItem('userToken'); @@ -20,8 +24,35 @@ const Header: React.FC = () => { else { console.error('Could not find token'); } + + // Check notification permission on load + if ('Notification' in window && navigator.serviceWorker) { + checkNotificationStatus(); + } }, []); + const checkNotificationStatus = async () => { + if ('serviceWorker' in navigator) { + try { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + setNotificationStatus(!!subscription); + } catch (error) { + console.error('Error checking notification status:', error); + } + } + }; + + const handleNotificationToggle = () => { + setIsNotificationModalOpen(!isNotificationModalOpen); + }; + + const handleCloseModal = () => { + setIsNotificationModalOpen(false); + // Check notification status again after modal is closed + checkNotificationStatus(); + }; + const handleLogout = async () => { try { localStorage.removeItem('userToken'); @@ -40,6 +71,19 @@ const Header: React.FC = () => {
+ {token && ( + + )} + {token ? (
{name_surname} @@ -58,6 +102,11 @@ const Header: React.FC = () => { )}
+ + ); }; diff --git a/src/feature/Header/components/NotificationModal.module.css b/src/feature/Header/components/NotificationModal.module.css new file mode 100644 index 0000000..79d54ee --- /dev/null +++ b/src/feature/Header/components/NotificationModal.module.css @@ -0,0 +1,113 @@ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: flex-start; + justify-content: flex-end; + z-index: 1000; +} + +.modalContent { + background: white; + border-radius: 5px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + width: 320px; + margin-top: 60px; + margin-right: 20px; + animation: slideIn 0.2s ease-out; + overflow: hidden; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modalHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + border-bottom: 1px solid #eee; +} + +.modalHeader h3 { + margin: 0; + font-size: 18px; +} + +.closeButton { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #555; +} + +.closeButton:hover { + color: #333; +} + +.statusContainer { + padding: 15px 20px; + border-bottom: 1px solid #eee; +} + +.statusContainer p { + margin: 5px 0; + font-size: 14px; +} + +.error { + padding: 10px 20px; + background-color: #fff0f0; + color: #e74c3c; + font-size: 14px; + border-bottom: 1px solid #eee; +} + +.actions { + padding: 15px 20px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.button { + padding: 8px 16px; + font-size: 14px; + border: none; + border-radius: 4px; + cursor: pointer; + background-color: #007bff; + color: white; + font-weight: 500; + transition: background-color 0.2s; +} + +.button:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +.button:hover:not(:disabled) { + background-color: #0069d9; +} + +.testButton { + background-color: #28a745; +} + +.testButton:hover { + background-color: #218838; +} \ No newline at end of file diff --git a/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.tsx b/src/feature/Header/components/NotificationModal.tsx similarity index 56% rename from src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.tsx rename to src/feature/Header/components/NotificationModal.tsx index ed4ac3d..d34e638 100644 --- a/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.tsx +++ b/src/feature/Header/components/NotificationModal.tsx @@ -1,32 +1,51 @@ import React, { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../../../../redux/store'; -import { API_BASE_URL } from '../../../../redux/Api/apiService'; -import styles from './NotificationTest.module.css'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../redux/store'; +import { API_BASE_URL } from '../../../redux/Api/apiService'; +import styles from './NotificationModal.module.css'; -const NotificationTest: React.FC = () => { +interface NotificationModalProps { + isOpen: boolean; + onClose: () => void; +} + +const NotificationModal: React.FC = ({ isOpen, onClose }) => { const [status, setStatus] = useState<'default' | 'granted' | 'denied'>('default'); const [isSubscribed, setIsSubscribed] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const token = useSelector((state: RootState) => state.user.token); - const dispatch = useDispatch(); useEffect(() => { if ('Notification' in window) { setStatus(Notification.permission); - console.log('Initial notification permission state:', Notification.permission); } }, []); + useEffect(() => { + // Check subscription status when modal opens + if (isOpen) { + checkSubscriptionStatus(); + } + }, [isOpen]); + + const checkSubscriptionStatus = async () => { + if ('serviceWorker' in navigator) { + try { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + setIsSubscribed(!!subscription); + } catch (error) { + console.error('Error checking subscription status:', error); + } + } + }; + const registerServiceWorker = async () => { try { - console.log('Registering service worker...'); const registration = await navigator.serviceWorker.register('/service-worker.js'); - console.log('Service worker registered successfully'); const subscription = await registration.pushManager.getSubscription(); - console.log('Current subscription status:', !!subscription); setIsSubscribed(!!subscription); return registration; } catch (error) { @@ -36,38 +55,22 @@ const NotificationTest: React.FC = () => { }; const urlBase64ToUint8Array = (base64String: string) => { - console.log('Converting base64 to Uint8Array. Raw input:', base64String); - - // 1. Check if the key is in PEM format (starts with MF...) + // Check if the key is in PEM format if (base64String.startsWith('MF')) { - console.log('Detected PEM format key, converting...'); - try { - // 2. Convert from standard base64 to a raw binary string const rawBinary = atob(base64String); - console.log('Length after standard base64 decode:', rawBinary.length); - - // 3. Extract key bits (65 bytes for P-256) const extractedKey = rawBinary.slice(-65); - console.log('Extracted key length:', extractedKey.length); - - // 4. Convert the binary string to Uint8Array const uint8Array = new Uint8Array(extractedKey.length); for (let i = 0; i < extractedKey.length; i++) { uint8Array[i] = extractedKey.charCodeAt(i); } - - console.log('Final Uint8Array length:', uint8Array.length); - console.log('First few bytes:', Array.from(uint8Array.slice(0, 5))); - return uint8Array; } catch (e) { - console.error('Error converting PEM format key:', e); throw new Error('Failed to convert applicationServerKey from PEM format'); } } - // Standard URL-safe base64 processing for already correctly formatted keys + // Standard URL-safe base64 processing let normalizedBase64 = base64String .replace(/-/g, '+') .replace(/_/g, '/'); @@ -78,24 +81,15 @@ const NotificationTest: React.FC = () => { normalizedBase64 += '='.repeat(4 - paddingNeeded); } - console.log('Processed base64 string:', normalizedBase64); - try { const binaryString = atob(normalizedBase64); - console.log('Decoded binary string length:', binaryString.length); - const uint8Array = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { uint8Array[i] = binaryString.charCodeAt(i); } - - console.log('Final Uint8Array length:', uint8Array.length); - console.log('First few bytes:', Array.from(uint8Array.slice(0, 5))); - return uint8Array; } catch (e) { - console.error('Error in standard base64 conversion:', e); - throw new Error('Failed to convert applicationServerKey: ' + e.message); + throw new Error(`Failed to convert applicationServerKey: ${e instanceof Error ? e.message : 'Unknown error'}`); } }; @@ -109,52 +103,41 @@ const NotificationTest: React.FC = () => { setError(null); try { - console.log('Requesting notification permission...'); const permission = await Notification.requestPermission(); - console.log('Permission response:', permission); setStatus(permission); if (permission !== 'granted') { throw new Error('Notification permission denied'); } - console.log('Registering service worker...'); const registration = await registerServiceWorker(); let subscription = await registration.pushManager.getSubscription(); if (!subscription) { - console.log('No existing subscription, fetching VAPID key...'); const response = await fetch(`${API_BASE_URL}/web-push/vapid-public-key`); - console.log('VAPID key response status:', response.status); if (!response.ok) { throw new Error('Failed to fetch VAPID key'); } const responseData = await response.json(); - console.log('VAPID key response data:', responseData); - const { publicKey } = responseData; + if (!publicKey) { throw new Error('Invalid VAPID key received from server'); } try { const applicationServerKey = urlBase64ToUint8Array(publicKey); - - console.log('Attempting to subscribe with converted key...'); subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }); - console.log('Subscription successful:', subscription); } catch (subError) { - console.error('Subscription error detail:', subError); throw new Error(`Failed to subscribe: ${subError instanceof Error ? subError.message : 'Unknown error'}`); } } - console.log('Sending subscription to server...'); const subscribeResponse = await fetch(`${API_BASE_URL}/web-push/subscribe`, { method: 'POST', headers: { @@ -163,7 +146,6 @@ const NotificationTest: React.FC = () => { }, body: JSON.stringify({ subscription: subscription.toJSON() }) }); - console.log('Server response status:', subscribeResponse.status); if (!subscribeResponse.ok) { throw new Error('Failed to register subscription with server'); @@ -171,9 +153,7 @@ const NotificationTest: React.FC = () => { setIsSubscribed(true); setError(null); - console.log('Subscription process completed successfully'); } catch (err) { - console.error('Subscription error:', err); setError(err instanceof Error ? err.message : 'Failed to subscribe to notifications'); setIsSubscribed(false); } finally { @@ -185,59 +165,63 @@ const NotificationTest: React.FC = () => { if (!token) return; try { - console.log('Sending test notification request...'); const response = await fetch(`${API_BASE_URL}/web-push/test`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); - console.log('Test notification response status:', response.status); if (!response.ok) { throw new Error('Failed to send test notification'); } - console.log('Test notification sent successfully'); } catch (error) { - console.error('Test notification error:', error); setError('Failed to send test notification'); } }; + if (!isOpen) return null; + return ( -
-

Notification Status

-
-

Permission: {status}

-

Subscribed: {isSubscribed ? 'Yes' : 'No'}

-
+
+
e.stopPropagation()}> +
+

Notification Status

+ +
- {error && ( -
- {error} +
+

Permission: {status}

+

Subscribed: {isSubscribed ? 'Yes' : 'No'}

- )} - -
- - - {isSubscribed && ( + + {error && ( +
+ {error} +
+ )} + +
- )} + + {isSubscribed && ( + + )} +
); }; -export default NotificationTest; \ No newline at end of file +export default NotificationModal; \ No newline at end of file diff --git a/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.module.css b/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.module.css deleted file mode 100644 index a81e7b9..0000000 --- a/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.container { - background: #ffffff; - border-radius: 8px; - padding: 1.5rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin: 1rem; -} - -.statusContainer { - margin: 1rem 0; - padding: 1rem; - background: #f5f5f5; - border-radius: 4px; -} - -.actions { - display: flex; - gap: 1rem; - margin-top: 1rem; -} - -.button { - padding: 0.75rem 1.5rem; - border: none; - border-radius: 4px; - background: #007bff; - color: white; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s; -} - -.button:hover { - background: #0056b3; -} - -.button:disabled { - background: #cccccc; - cursor: not-allowed; -} - -.testButton { - background: #28a745; -} - -.testButton:hover { - background: #218838; -} - -.error { - color: #dc3545; - padding: 0.75rem; - margin: 1rem 0; - background: #f8d7da; - border-radius: 4px; - border: 1px solid #f5c6cb; -} \ No newline at end of file diff --git a/src/feature/RestaurantDetails/screens/RestaurantDetails.tsx b/src/feature/RestaurantDetails/screens/RestaurantDetails.tsx index 92478b8..ce7556f 100644 --- a/src/feature/RestaurantDetails/screens/RestaurantDetails.tsx +++ b/src/feature/RestaurantDetails/screens/RestaurantDetails.tsx @@ -7,7 +7,6 @@ import Orders from "../components/Orders/Orders"; import Comments from "../components/Comments/Comments"; import Analytics from "../components/Analytics/Analytics"; import { IoRestaurantOutline, IoListOutline, IoReceiptOutline, IoChatbubbleOutline, IoStatsChartOutline } from 'react-icons/io5'; -import NotificationTest from "../components/NotificationTest/NotificationTest.tsx"; const RestaurantDetails: React.FC = () => { const { restaurantId } = useParams<{ restaurantId: string }>(); @@ -18,7 +17,6 @@ const RestaurantDetails: React.FC = () => { return (
-