From 26eb7cb8986386eafb336d93a57cac2f610c5837 Mon Sep 17 00:00:00 2001 From: dwindown Date: Tue, 11 Nov 2025 15:15:02 +0700 Subject: [PATCH] feat: Implement push notification settings backend and UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ✅ 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** 📧 --- .../Settings/Notifications/ChannelConfig.tsx | 103 +++++++++++++++--- includes/Api/NotificationsController.php | 64 +++++++++++ .../Notifications/PushNotificationHandler.php | 68 ++++++++++++ 3 files changed, 222 insertions(+), 13 deletions(-) diff --git a/admin-spa/src/routes/Settings/Notifications/ChannelConfig.tsx b/admin-spa/src/routes/Settings/Notifications/ChannelConfig.tsx index cf382bd..2e24428 100644 --- a/admin-spa/src/routes/Settings/Notifications/ChannelConfig.tsx +++ b/admin-spa/src/routes/Settings/Notifications/ChannelConfig.tsx @@ -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 ( @@ -81,7 +123,12 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel } -
+ {isLoading ? ( +
+ +
+ ) : ( +
{/* Branding */}
@@ -93,7 +140,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel } {__('Display your store logo in push notifications')}

-
@@ -103,7 +154,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel } {__('Show product images in order notifications')}

- + setSettings({...settings, use_product_images: checked})} + />
@@ -113,7 +168,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel } {__('Display customer avatar when available')}

- + setSettings({...settings, use_gravatar: checked})} + /> @@ -127,7 +186,8 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel } setSettings({...settings, click_action: e.target.value})} />

{__('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')}

- + setSettings({...settings, require_interaction: checked})} + />
@@ -151,7 +215,11 @@ export default function ChannelConfig({ open, onClose, channelId, channelLabel } {__('Disable notification sound')}

- + setSettings({...settings, silent: checked})} + /> @@ -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.')}

- + + ) + }
- -
diff --git a/includes/Api/NotificationsController.php b/includes/Api/NotificationsController.php index dbee244..e343c3f 100644 --- a/includes/Api/NotificationsController.php +++ b/includes/Api/NotificationsController.php @@ -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); + } } diff --git a/includes/Core/Notifications/PushNotificationHandler.php b/includes/Core/Notifications/PushNotificationHandler.php index f38ee89..967b18d 100644 --- a/includes/Core/Notifications/PushNotificationHandler.php +++ b/includes/Core/Notifications/PushNotificationHandler.php @@ -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); } /**