littleshop/LittleShop/wwwroot/js/notifications.js
SysAdmin cd479d8946 Fix: Prevent notification prompt from reappearing after timeout
**Issue:**
- Notification prompt kept reappearing after push subscription timeout
- Users stuck in loop when push notifications fail due to network restrictions

**Solution:**
- Auto-dismiss prompt on timeout errors
- Mark as permanently declined when timeout occurs
- Provide user-friendly error message
- Clean up error handling flow

**Technical Changes:**
- Check for timeout in error message
- Set both session and permanent dismissal flags
- Simplify error propagation from enableNotifications()
- Show concise error message for timeout scenarios

This fix ensures users in restricted network environments (VPNs, corporate firewalls, FCM blocked) won't be repeatedly prompted for push notifications that can't work.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-02 14:44:10 +01:00

366 lines
13 KiB
JavaScript

// Enhanced notification management for LittleShop Admin
// Handles real-time order notifications and admin alerts
class AdminNotificationManager {
constructor() {
this.isSetupComplete = false;
this.notificationQueue = [];
this.init();
}
async init() {
console.log('Admin Notifications: Initializing...');
// Wait for PWA manager to be ready
if (window.pwaManager) {
await this.setupOrderNotifications();
} else {
// Wait for PWA manager to load
setTimeout(() => this.init(), 1000);
}
}
async setupOrderNotifications() {
try {
// Ensure push notifications are enabled
if (!window.pwaManager.pushSubscription) {
console.log('Admin Notifications: Setting up push notifications...');
// Show admin-specific notification prompt
this.showAdminNotificationPrompt();
return;
}
this.isSetupComplete = true;
this.addNotificationStatusIndicator();
this.setupTestNotificationButton();
console.log('Admin Notifications: Setup complete');
} catch (error) {
console.error('Admin Notifications: Setup failed:', error);
}
}
showAdminNotificationPrompt() {
// Check if notifications are supported
if (!('Notification' in window) || !('PushManager' in window)) {
console.log('Admin Notifications: Push notifications not supported in this browser');
return;
}
// Check if already enabled
if (window.pwaManager && window.pwaManager.pushSubscription) {
console.log('Admin Notifications: Already subscribed');
return;
}
// Check if prompt already exists
if (document.getElementById('admin-notification-prompt')) {
return;
}
// Check if dismissed in current session
const sessionDismissed = sessionStorage.getItem('notificationPromptDismissed');
if (sessionDismissed === 'true') {
console.log('Admin Notifications: Prompt dismissed this session');
return;
}
// Check if permanently dismissed
const permanentlyDismissed = localStorage.getItem('pushNotificationDeclined');
if (permanentlyDismissed === 'true') {
console.log('Admin Notifications: Notifications declined permanently');
return;
}
const promptDiv = document.createElement('div');
promptDiv.id = 'admin-notification-prompt';
promptDiv.className = 'alert alert-warning alert-dismissible position-fixed';
promptDiv.style.cssText = `
top: 80px;
right: 20px;
z-index: 1055;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
promptDiv.innerHTML = `
<div class="d-flex align-items-center">
<i class="fas fa-bell-slash text-warning me-3 fa-2x"></i>
<div class="flex-grow-1">
<h6 class="alert-heading mb-1">Enable Order Notifications</h6>
<p class="mb-2">Get instant alerts for new orders, payments, and status changes.</p>
<div class="d-flex gap-2">
<button type="button" class="btn btn-warning btn-sm" id="enable-admin-notifications">
<i class="fas fa-bell me-1"></i>Enable Now
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="remind-later">
Later
</button>
</div>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" id="close-notification-prompt"></button>
`;
document.body.appendChild(promptDiv);
// Add event listeners
document.getElementById('enable-admin-notifications').addEventListener('click', async () => {
try {
await this.enableNotifications();
promptDiv.remove();
sessionStorage.setItem('notificationPromptDismissed', 'true');
} catch (error) {
console.error('Failed to enable notifications:', error);
// Check if it's a timeout error - if so, dismiss the prompt
if (error.message && error.message.includes('timed out')) {
promptDiv.remove();
sessionStorage.setItem('notificationPromptDismissed', 'true');
localStorage.setItem('pushNotificationDeclined', 'true');
this.showNotificationError('Push notifications timed out. This may be due to network restrictions. The app will work without push notifications.');
} else {
this.showNotificationError('Failed to enable notifications. Please try again.');
}
}
});
document.getElementById('remind-later').addEventListener('click', () => {
promptDiv.remove();
// Mark as dismissed for this session only
sessionStorage.setItem('notificationPromptDismissed', 'true');
});
document.getElementById('close-notification-prompt').addEventListener('click', () => {
promptDiv.remove();
// Mark as dismissed for this session only
sessionStorage.setItem('notificationPromptDismissed', 'true');
});
}
async enableNotifications() {
const button = document.getElementById('enable-admin-notifications');
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enabling...';
try {
await window.pwaManager.subscribeToPushNotifications();
// Show success message
this.showNotificationSuccess('✅ Order notifications enabled successfully!');
// Complete setup
await this.setupOrderNotifications();
} catch (error) {
console.error('Failed to enable notifications:', error);
// Re-throw so the caller can handle it
throw error;
} finally {
if (button) {
button.disabled = false;
button.innerHTML = originalText;
}
}
}
addNotificationStatusIndicator() {
// Add status indicator to admin header/navbar
const navbar = document.querySelector('.navbar-nav');
if (!navbar || document.getElementById('notification-status')) {
return;
}
const statusItem = document.createElement('li');
statusItem.className = 'nav-item dropdown';
statusItem.innerHTML = `
<a class="nav-link dropdown-toggle" href="#" id="notification-status" role="button" data-bs-toggle="dropdown">
<i class="fas fa-bell text-success"></i>
<span class="d-none d-md-inline ms-1">Notifications</span>
<span id="notification-badge" class="badge bg-danger ms-1" style="display: none;">0</span>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><h6 class="dropdown-header">Notification Settings</h6></li>
<li><a class="dropdown-item" href="#" id="test-notification">
<i class="fas fa-vial me-2"></i>Send Test Notification
</a></li>
<li><a class="dropdown-item" href="#" id="notification-history">
<i class="fas fa-history me-2"></i>Recent Notifications
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" id="disable-notifications">
<i class="fas fa-bell-slash me-2"></i>Disable Notifications
</a></li>
</ul>
`;
navbar.appendChild(statusItem);
// Add event listeners
document.getElementById('test-notification').addEventListener('click', (e) => {
e.preventDefault();
this.sendTestNotification();
});
document.getElementById('disable-notifications').addEventListener('click', (e) => {
e.preventDefault();
this.disableNotifications();
});
}
setupTestNotificationButton() {
// Add test button to dashboard if we're on the dashboard page
const dashboardContent = document.querySelector('.dashboard-content, .admin-dashboard');
if (!dashboardContent) {
return;
}
const testButton = document.createElement('button');
testButton.className = 'btn btn-outline-primary btn-sm me-2';
testButton.innerHTML = '<i class="fas fa-bell me-1"></i>Test Notification';
testButton.onclick = () => this.sendTestNotification();
// Find a good place to add it (e.g., near page title)
const pageTitle = document.querySelector('h1, .page-title');
if (pageTitle) {
pageTitle.parentNode.insertBefore(testButton, pageTitle.nextSibling);
}
}
async sendTestNotification() {
try {
const response = await fetch('/api/push/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: '🧪 Test Notification',
body: 'LittleShop admin notifications are working perfectly!'
}),
credentials: 'same-origin'
});
if (response.ok) {
this.showNotificationSuccess('Test notification sent!');
} else {
throw new Error('Failed to send test notification');
}
} catch (error) {
console.error('Test notification failed:', error);
this.showNotificationError('Failed to send test notification');
}
}
async disableNotifications() {
if (confirm('Are you sure you want to disable order notifications?')) {
try {
await window.pwaManager.unsubscribeFromPushNotifications();
// Remove status indicator
const statusElement = document.getElementById('notification-status');
if (statusElement) {
statusElement.closest('.nav-item').remove();
}
this.showNotificationSuccess('Notifications disabled');
// Reset setup status
this.isSetupComplete = false;
} catch (error) {
console.error('Failed to disable notifications:', error);
this.showNotificationError('Failed to disable notifications');
}
}
}
showNotificationSuccess(message) {
this.showToast(message, 'success');
}
showNotificationError(message) {
this.showToast(message, 'danger');
}
showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `alert alert-${type} alert-dismissible position-fixed`;
toast.style.cssText = `
top: 20px;
right: 20px;
z-index: 1060;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
toast.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 5000);
}
// Handle incoming notifications (if using WebSocket/SignalR in future)
handleOrderNotification(data) {
if (!this.isSetupComplete) {
this.notificationQueue.push(data);
return;
}
// Update notification badge
this.updateNotificationBadge();
// Show browser notification if page is not visible
if (document.hidden && window.pwaManager) {
window.pwaManager.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/icon-72x72.png',
tag: 'order-notification',
requireInteraction: true,
actions: [
{ action: 'view', title: 'View Order' },
{ action: 'dismiss', title: 'Dismiss' }
]
});
}
}
updateNotificationBadge(count = null) {
const badge = document.getElementById('notification-badge');
if (!badge) return;
if (count === null) {
// Get current count and increment
const currentCount = parseInt(badge.textContent) || 0;
count = currentCount + 1;
}
if (count > 0) {
badge.textContent = count;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
}
// Initialize admin notification manager
document.addEventListener('DOMContentLoaded', () => {
window.adminNotificationManager = new AdminNotificationManager();
});
// Export for global access
window.AdminNotificationManager = AdminNotificationManager;