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,6 +123,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
</DialogDescription>
</DialogHeader>
{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">
@@ -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>
@@ -162,13 +230,22 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel }
</p>
</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>

View File

@@ -120,6 +120,24 @@ class NotificationsController {
'permission_callback' => '__return_true',
],
]);
// GET /woonoow/v1/notifications/push/settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/push/settings', [
[
'methods' => 'GET',
'callback' => [$this, 'get_push_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
// POST /woonoow/v1/notifications/push/settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/push/settings', [
[
'methods' => 'POST',
'callback' => [$this, 'update_push_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
}
/**
@@ -468,4 +486,50 @@ class NotificationsController {
'message' => __('Unsubscribed from push notifications', 'woonoow'),
], 200);
}
/**
* Get push notification settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
public function get_push_settings(WP_REST_Request $request) {
$settings = PushNotificationHandler::get_settings();
return new WP_REST_Response($settings, 200);
}
/**
* Update push notification settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
public function update_push_settings(WP_REST_Request $request) {
$settings = $request->get_json_params();
if (empty($settings)) {
return new WP_Error(
'invalid_settings',
__('Settings data is required', 'woonoow'),
['status' => 400]
);
}
$success = PushNotificationHandler::update_settings($settings);
if (!$success) {
return new WP_Error(
'update_failed',
__('Failed to update push notification settings', 'woonoow'),
['status' => 500]
);
}
return new WP_REST_Response([
'success' => true,
'message' => __('Push notification settings updated', 'woonoow'),
'settings' => PushNotificationHandler::get_settings(),
], 200);
}
}

View File

@@ -21,12 +21,80 @@ class PushNotificationHandler {
*/
const VAPID_KEYS_KEY = 'woonoow_push_vapid_keys';
/**
* Option key for push settings
*/
const SETTINGS_KEY = 'woonoow_push_settings';
/**
* Initialize push notifications
*/
public static function init() {
// Generate VAPID keys if not exists
self::ensure_vapid_keys();
// Ensure default settings exist
self::ensure_default_settings();
}
/**
* Ensure default push settings exist
*
* @return array
*/
public static function ensure_default_settings() {
$settings = get_option(self::SETTINGS_KEY);
if (!$settings) {
$settings = self::get_default_settings();
update_option(self::SETTINGS_KEY, $settings);
}
return $settings;
}
/**
* Get default push settings
*
* @return array
*/
public static function get_default_settings() {
return [
'use_logo' => true,
'use_product_images' => true,
'use_gravatar' => false,
'click_action' => '/wp-admin/admin.php?page=woonoow#/orders',
'require_interaction' => false,
'silent' => false,
];
}
/**
* Get push settings
*
* @return array
*/
public static function get_settings() {
$settings = get_option(self::SETTINGS_KEY);
if (!$settings) {
$settings = self::get_default_settings();
}
return $settings;
}
/**
* Update push settings
*
* @param array $settings
* @return bool
*/
public static function update_settings($settings) {
$current = self::get_settings();
$updated = array_merge($current, $settings);
return update_option(self::SETTINGS_KEY, $updated);
}
/**