feat: Implement push notification settings backend and UI

##  Push Notification Settings - Fully Functional

### Backend (PHP)

**PushNotificationHandler Updates:**
- Added `SETTINGS_KEY` constant
- `ensure_default_settings()` - Initialize defaults
- `get_default_settings()` - Return default config
- `get_settings()` - Fetch current settings
- `update_settings()` - Save settings

**Default Settings:**
```php
[
  'use_logo' => true,
  'use_product_images' => true,
  'use_gravatar' => false,
  'click_action' => '/wp-admin/admin.php?page=woonoow#/orders',
  'require_interaction' => false,
  'silent' => false,
]
```

**NotificationsController:**
- `GET /notifications/push/settings` - Fetch settings
- `POST /notifications/push/settings` - Update settings
- Permission-protected endpoints

### Frontend (React)

**ChannelConfig Component:**
- Fetches push settings on open
- Real-time state management
- Connected switches and inputs
- Save mutation with loading state
- Toast notifications for success/error
- Disabled state during save

**Settings Available:**
1. **Branding**
   - Use Store Logo
   - Use Product Images
   - Use Customer Gravatar

2. **Behavior**
   - Click Action URL (input)
   - Require Interaction
   - Silent Notifications

### Features

 **Backend Storage** - Settings saved in wp_options
 **REST API** - GET and POST endpoints
 **Frontend UI** - Full CRUD interface
 **State Management** - React Query integration
 **Loading States** - Skeleton and button states
 **Error Handling** - Toast notifications
 **Default Values** - Sensible defaults

---

**Next: Email channel toggle** 📧
This commit is contained in:
dwindown
2025-11-11 15:15:02 +07:00
parent 63dbed757a
commit 26eb7cb898
3 changed files with 222 additions and 13 deletions

View File

@@ -1,4 +1,6 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import {
Dialog,
DialogContent,
@@ -10,7 +12,8 @@ import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { ExternalLink } from 'lucide-react';
import { ExternalLink, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { __ } from '@/lib/i18n';
interface ChannelConfigProps {
@@ -71,6 +74,45 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
// Push notification configuration
if (channelId === 'push') {
const queryClient = useQueryClient();
const [settings, setSettings] = useState({
use_logo: true,
use_product_images: true,
use_gravatar: false,
click_action: '/wp-admin/admin.php?page=woonoow#/orders',
require_interaction: false,
silent: false,
});
// Fetch push settings
const { data: pushSettings, isLoading } = useQuery({
queryKey: ['push-settings'],
queryFn: () => api.get('/notifications/push/settings'),
enabled: open,
});
// Update local state when data is fetched
useEffect(() => {
if (pushSettings) {
setSettings(pushSettings);
}
}, [pushSettings]);
// Save settings mutation
const saveMutation = useMutation({
mutationFn: async () => {
return api.post('/notifications/push/settings', settings);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['push-settings'] });
toast.success(__('Push notification settings saved'));
onClose();
},
onError: (error: any) => {
toast.error(error?.message || __('Failed to save settings'));
},
});
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
@@ -81,7 +123,12 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="space-y-6 py-4">
{/* Branding */}
<div className="space-y-3">
<Label>{__('Notification Branding')}</Label>
@@ -93,7 +140,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
{__('Display your store logo in push notifications')}
</p>
</div>
<Switch id="use-logo" defaultChecked />
<Switch
id="use-logo"
checked={settings.use_logo}
onCheckedChange={(checked) => setSettings({...settings, use_logo: checked})}
/>
</div>
<div className="flex items-center justify-between">
@@ -103,7 +154,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
{__('Show product images in order notifications')}
</p>
</div>
<Switch id="use-product-image" defaultChecked />
<Switch
id="use-product-image"
checked={settings.use_product_images}
onCheckedChange={(checked) => setSettings({...settings, use_product_images: checked})}
/>
</div>
<div className="flex items-center justify-between">
@@ -113,7 +168,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
{__('Display customer avatar when available')}
</p>
</div>
<Switch id="use-gravatar" />
<Switch
id="use-gravatar"
checked={settings.use_gravatar}
onCheckedChange={(checked) => setSettings({...settings, use_gravatar: checked})}
/>
</div>
</div>
</div>
@@ -127,7 +186,8 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
<Input
id="click-action"
placeholder={__('https://yourstore.com/orders')}
defaultValue="/wp-admin/admin.php?page=woonoow#/orders"
value={settings.click_action}
onChange={(e) => setSettings({...settings, click_action: e.target.value})}
/>
<p className="text-xs text-muted-foreground">
{__('Where users are redirected when clicking the notification')}
@@ -141,7 +201,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
{__('Notification stays until user dismisses it')}
</p>
</div>
<Switch id="require-interaction" />
<Switch
id="require-interaction"
checked={settings.require_interaction}
onCheckedChange={(checked) => setSettings({...settings, require_interaction: checked})}
/>
</div>
<div className="flex items-center justify-between">
@@ -151,7 +215,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
{__('Disable notification sound')}
</p>
</div>
<Switch id="silent" />
<Switch
id="silent"
checked={settings.silent}
onCheckedChange={(checked) => setSettings({...settings, silent: checked})}
/>
</div>
</div>
</div>
@@ -161,14 +229,23 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
💡 {__('Note: These settings will be saved and applied to all push notifications. Individual templates can override the icon and image.')}
</p>
</div>
</div>
</div>
)
}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose}>
<Button variant="outline" onClick={onClose} disabled={saveMutation.isPending}>
{__('Cancel')}
</Button>
<Button onClick={onClose}>
{__('Save Configuration')}
<Button onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending || isLoading}>
{saveMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{__('Saving...')}
</>
) : (
__('Save Configuration')
)}
</Button>
</div>
</DialogContent>