react-notify-sdk 1.0.31 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { NotifyMessage } from './types';
2
+ import { FeatureMessage } from './types';
3
3
  interface FadeWrapperProps {
4
4
  visible: boolean;
5
- message: NotifyMessage;
5
+ message: FeatureMessage;
6
6
  device: string;
7
7
  }
8
8
  export declare const FadeWrapper: import("goober").StyledVNode<Omit<React.ClassAttributes<HTMLDivElement> & React.HTMLAttributes<HTMLDivElement> & import("goober").DefaultTheme & FadeWrapperProps, never>>;
@@ -1,6 +1,6 @@
1
- import { NotifyMessage } from './types';
1
+ import { FeatureMessage } from './types';
2
2
  interface NotifyMessageProps {
3
- message: NotifyMessage;
3
+ message: FeatureMessage;
4
4
  }
5
5
  declare const NotificationMessage: ({ message }: NotifyMessageProps) => import("react/jsx-runtime").JSX.Element;
6
6
  export default NotificationMessage;
@@ -1,7 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import FadeWrapper from './FadeWrapper';
2
3
  /////////////---Icon imports---////////////////////
3
4
  import { AlertTriangle, CircleX, Info } from "lucide-react";
5
+ import useDeviceDetection from './UseDeviceDetection';
4
6
  const NotificationMessage = ({ message }) => {
7
+ const device = useDeviceDetection();
5
8
  function hexToRgba(hex, alpha) {
6
9
  // Remove leading #
7
10
  hex = hex.replace(/^#/, "");
@@ -16,31 +19,31 @@ const NotificationMessage = ({ message }) => {
16
19
  const b = bigint & 255;
17
20
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
18
21
  }
19
- return (_jsxs("div", { style: {
20
- width: '100%',
21
- paddingTop: '4px', // p-4 = 1rem
22
- paddingBottom: '4px',
23
- paddingLeft: '12px',
24
- paddingRight: '12px',
25
- backgroundColor: message?.backgroundColor,
26
- borderRadius: "8px",
27
- borderWidth: "1px",
28
- borderColor: `${hexToRgba(message.borderColor, 0.3)}`,
29
- boxShadow: `0 10px 15px -3px ${hexToRgba(message.borderColor, 0.4)}, 0 4px 6px -4px ${hexToRgba(message.borderColor, 0.4)}`
30
- }, children: [_jsxs("div", { className: 'w-full flex flex-row items-center', style: {
31
- width: '100%',
32
- display: 'flex',
33
- flexDirection: 'row',
34
- alignItems: 'center'
35
- }, children: [message?.type === 'Information' && _jsx(Info, { size: 16, style: { color: 'rgb(59 130 24)' } }), message?.type === 'Warning' && _jsx(AlertTriangle, { size: 16, style: { color: 'rgb(245 158 1)' } }), message?.type === 'Error' && _jsx(CircleX, { size: 16, style: { color: 'rgb(239 68 68)' } }), _jsx("p", { className: `text-lg font-semibold text-[${message?.textColor}] ml-3`, style: {
36
- fontSize: '1.125rem', // text-lg = 18px = 1.125rem
37
- fontWeight: 600, // font-semibold = 600
38
- color: `${message?.textColor}`,
39
- marginLeft: '12px'
40
- }, children: message.title })] }), _jsx("p", { className: `text-sm text-[${message?.textColor}]`, style: {
41
- marginTop: '8px',
42
- fontSize: '0.875rem', // text-sm = 14px = 0.875rem
43
- color: `${message?.textColor}`,
44
- }, children: message.content })] }));
22
+ return (_jsx(FadeWrapper, { visible: message?.is_active, device: device, message: message, children: _jsxs("div", { style: {
23
+ width: '100%',
24
+ paddingTop: '4px', // p-4 = 1rem
25
+ paddingBottom: '4px',
26
+ paddingLeft: '12px',
27
+ paddingRight: '12px',
28
+ backgroundColor: message?.backgroundColor,
29
+ borderRadius: "8px",
30
+ borderWidth: "1px",
31
+ borderColor: `${hexToRgba(message.borderColor, 0.3)}`,
32
+ boxShadow: `0 10px 15px -3px ${hexToRgba(message.borderColor, 0.4)}, 0 4px 6px -4px ${hexToRgba(message.borderColor, 0.4)}`
33
+ }, children: [_jsxs("div", { className: 'w-full flex flex-row items-center', style: {
34
+ width: '100%',
35
+ display: 'flex',
36
+ flexDirection: 'row',
37
+ alignItems: 'center'
38
+ }, children: [message?.type === 'Information' && _jsx(Info, { size: 16, style: { color: 'rgb(59 130 24)' } }), message?.type === 'Warning' && _jsx(AlertTriangle, { size: 16, style: { color: 'rgb(245 158 1)' } }), message?.type === 'Error' && _jsx(CircleX, { size: 16, style: { color: 'rgb(239 68 68)' } }), _jsx("p", { className: `text-lg font-semibold text-[${message?.textColor}] ml-3`, style: {
39
+ fontSize: '1.125rem', // text-lg = 18px = 1.125rem
40
+ fontWeight: 600, // font-semibold = 600
41
+ color: `${message?.textColor}`,
42
+ marginLeft: '12px'
43
+ }, children: message.title })] }), _jsx("p", { className: `text-sm text-[${message?.textColor}]`, style: {
44
+ marginTop: '8px',
45
+ fontSize: '0.875rem', // text-sm = 14px = 0.875rem
46
+ color: `${message?.textColor}`,
47
+ }, children: message.content })] }) }));
45
48
  };
46
49
  export default NotificationMessage;
@@ -1,2 +1,12 @@
1
- import { NotifyMessageProviderProps } from './types';
2
- export declare const NotifyProvider: ({ projectKey, }: NotifyMessageProviderProps) => import("react/jsx-runtime").JSX.Element | null;
1
+ import { FeatureMessageProviderProps } from './types';
2
+ import { FeatureMessagesConfig } from './supabase/supabaseClient';
3
+ interface FeatureMessagesContextType {
4
+ config: FeatureMessagesConfig | null;
5
+ projectKey: string | null;
6
+ }
7
+ export declare const NotifyProvider: ({ children, projectKey, endpoint, }: FeatureMessageProviderProps) => import("react/jsx-runtime").JSX.Element | null;
8
+ /**
9
+ * Hook to get the feature messages context (for advanced use cases)
10
+ */
11
+ export declare const useFeatureMessagesContext: () => FeatureMessagesContextType;
12
+ export {};
@@ -1,83 +1,78 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/FeatureMessage.tsx
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useState, createContext, useContext } from 'react';
4
4
  import { useLocation } from 'react-router-dom';
5
5
  import NotificationMessage from './NotifyMessage';
6
6
  import { getSupabaseClient } from './supabase/supabaseClient';
7
+ import { useFeatureMessages } from './useFeatureMessage';
7
8
  import useDeviceDetection from './UseDeviceDetection';
8
- import FadeWrapper from './FadeWrapper';
9
- // Wildcard route matching utility
10
- const routeMatches = (pattern, path) => {
11
- if (pattern === path)
12
- return true;
13
- const escapeRegex = (str) => str.replace(/([.+^=!:${}()|[\]/\\])/g, "\\$1");
14
- const regexPattern = "^" + pattern.split("*").map(escapeRegex).join(".*") + "$";
15
- try {
16
- return new RegExp(regexPattern).test(path);
17
- }
18
- catch {
19
- return false;
20
- }
21
- };
22
- export const NotifyProvider = ({ projectKey,
23
- //className,
24
- //style,
25
- //disableDefaultStyles = true,
26
- }) => {
9
+ const FeatureMessagesContext = createContext({
10
+ config: null,
11
+ projectKey: null,
12
+ });
13
+ export const NotifyProvider = ({ children, projectKey, endpoint, }) => {
14
+ const config = { projectKey, endpoint };
15
+ useEffect(() => {
16
+ getSupabaseClient(config);
17
+ }, [projectKey, endpoint]);
27
18
  const [message, setMessage] = useState(null);
28
19
  const [visible, setVisible] = useState(false);
29
20
  const location = useLocation();
30
21
  const device = useDeviceDetection();
31
- const supabase = getSupabaseClient();
22
+ if (!message)
23
+ return null;
24
+ return (_jsxs(FeatureMessagesContext.Provider, { value: { config, projectKey }, children: [children, _jsx(AutomaticFeatureMessages, { projectKey: projectKey })] }));
25
+ /*
26
+ return message ? (
27
+ <FadeWrapper visible={visible} device={device} message={message}>
28
+ <NotificationMessage message={message} />
29
+ </FadeWrapper>
30
+ )
31
+ :
32
+ null;
33
+ */
34
+ };
35
+ /**
36
+ * Internal component that automatically displays messages based on current route
37
+ */
38
+ const AutomaticFeatureMessages = ({ projectKey }) => {
39
+ // Get current route - works with any router or vanilla JavaScript
40
+ const [currentRoute, setCurrentRoute] = useState(window.location.pathname);
32
41
  useEffect(() => {
33
- if (typeof window === 'undefined')
34
- return; // Skip on server
35
- const fetchMessage = async () => {
36
- const { data } = await supabase
37
- .from('feature_messages')
38
- .select('*')
39
- .eq('project_key', projectKey)
40
- .eq('route', location.pathname)
41
- .eq('is_active', true)
42
- .order('created_at', { ascending: false });
43
- //.limit(1)
44
- //.single();
45
- const match = data?.find((msg) => routeMatches(msg.route, location.pathname));
46
- if (match) {
47
- setMessage(match);
48
- setVisible(true); // Fade in
49
- }
50
- else {
51
- // Trigger fade out before removing message
52
- setVisible(false);
53
- setTimeout(() => setMessage(null), 300); // match fadeOut duration
54
- }
42
+ // Listen for route changes (works with React Router, Next.js Router, etc.)
43
+ const handleRouteChange = () => {
44
+ setCurrentRoute(window.location.pathname);
45
+ };
46
+ // Listen for browser navigation
47
+ window.addEventListener('popstate', handleRouteChange);
48
+ // Listen for programmatic navigation (works with most routers)
49
+ const originalPushState = history.pushState;
50
+ const originalReplaceState = history.replaceState;
51
+ history.pushState = function () {
52
+ originalPushState.apply(history, arguments);
53
+ setTimeout(handleRouteChange, 0);
54
+ };
55
+ history.replaceState = function () {
56
+ originalReplaceState.apply(history, arguments);
57
+ setTimeout(handleRouteChange, 0);
55
58
  };
56
- fetchMessage();
57
- const subscription = supabase
58
- .channel('feature_messages_channel')
59
- .on('postgres_changes', {
60
- event: '*',
61
- schema: 'public',
62
- table: 'feature_messages',
63
- }, (payload) => {
64
- const msg = payload.new;
65
- if (msg.project_key === projectKey && msg.is_active && routeMatches(msg.route, location.pathname)) {
66
- setMessage(msg);
67
- setVisible(true);
68
- }
69
- else {
70
- setVisible(false);
71
- setTimeout(() => setMessage(null), 300);
72
- }
73
- })
74
- .subscribe();
75
59
  return () => {
76
- supabase.removeChannel(subscription);
60
+ window.removeEventListener('popstate', handleRouteChange);
61
+ history.pushState = originalPushState;
62
+ history.replaceState = originalReplaceState;
77
63
  };
78
- }, [location.pathname, projectKey]);
79
- if (!message)
80
- return null;
81
- //const defaultClasses = `absolute ${message?.position}-6 left-1/2 transform -translate-x-1/2 z-20 w-full md:w-[${message?.width}%] px-3 py-1 bg-[${message?.backgroundColor}] border border-[${message?.borderColor}] text-[${message?.textColor}] rounded-lg shadow`;
82
- return (_jsx(FadeWrapper, { visible: visible, device: device, message: message, children: _jsx(NotificationMessage, { message: message }) }));
64
+ }, []);
65
+ // Use the internal hook to get messages for current route
66
+ const { message, dismissMessage } = useFeatureMessages({
67
+ projectKey,
68
+ route: currentRoute,
69
+ });
70
+ // Automatically render the message if one exists
71
+ return message ? (_jsx(NotificationMessage, { message: message })) : null;
72
+ };
73
+ /**
74
+ * Hook to get the feature messages context (for advanced use cases)
75
+ */
76
+ export const useFeatureMessagesContext = () => {
77
+ return useContext(FeatureMessagesContext);
83
78
  };
@@ -5,9 +5,9 @@ const useDeviceDetection = () => {
5
5
  useEffect(() => {
6
6
  const handleDeviceDetection = () => {
7
7
  const parser = new UAParser(navigator.userAgent);
8
- const parsedDevice = parser.getDevice();
9
- const isMobile = parsedDevice.type === 'mobile';
10
- const isTablet = parsedDevice.type === 'tablet';
8
+ const parsedDevice = parser.getResult();
9
+ const isMobile = parsedDevice.device.type === 'mobile';
10
+ const isTablet = parsedDevice.device.type === 'tablet';
11
11
  const isDesktop = !isMobile && !isTablet;
12
12
  if (isMobile) {
13
13
  setDevice('Mobile');
@@ -1,2 +1,32 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
2
- export declare const getSupabaseClient: () => SupabaseClient<any, "public", any>;
2
+ export interface FeatureMessagesConfig {
3
+ /**
4
+ * Your SaaS API key (provided to customers)
5
+ * This identifies the customer and their project
6
+ */
7
+ projectKey: string;
8
+ /**
9
+ * Optional: Custom endpoint if you're using a different domain
10
+ * Defaults to your SaaS service endpoint
11
+ */
12
+ endpoint?: string;
13
+ /**
14
+ * Optional: Enable debug logging
15
+ */
16
+ debug?: boolean;
17
+ }
18
+ export declare const getSupabaseClient: (config: FeatureMessagesConfig) => SupabaseClient;
19
+ /**
20
+ * Get the initialized feature messages client
21
+ * Used internally by the package components and hooks
22
+ */
23
+ export declare const getFeatureMessagesClient: () => SupabaseClient;
24
+ /**
25
+ * Get the current customer configuration
26
+ * Used internally by the package
27
+ */
28
+ export declare const getCustomerConfig: () => FeatureMessagesConfig | null;
29
+ /**
30
+ * Reset the client (useful for testing or switching configurations)
31
+ */
32
+ export declare const resetFeatureMessagesClient: () => void;
@@ -1,18 +1,65 @@
1
1
  import { createClient } from '@supabase/supabase-js';
2
- const supabaseUrl = process.env.REACT_APP_SUPABASE_URL;
3
- const supabaseKey = process.env.REACT_APP_SUPABASE_KEY;
2
+ const supabaseUrl = process.env.REACT_APP_SUPABASE_URL || "https://dhgnstjrkeuqnsapwcec.supabase.co";
3
+ const supabaseKey = process.env.REACT_APP_SUPABASE_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRoZ25zdGpya2V1cW5zYXB3Y2VjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTA2MDA0NTEsImV4cCI6MjA2NjE3NjQ1MX0.TLBAPIWjPYvzNUUPHyakIypRydAeWM8Y1OrUzVvIgLQ";
4
4
  // Create a single supabase client for interacting with your database
5
5
  //export const supabase = createClient(supabaseUrl, supabaseKey)
6
- let client = null;
7
- export const getSupabaseClient = () => {
8
- if (!client) {
9
- client = createClient(supabaseUrl, supabaseKey, {
6
+ let featureMessagesClient = null;
7
+ let customerConfig = null;
8
+ export const getSupabaseClient = (config) => {
9
+ if (!config.projectKey) {
10
+ throw new Error('Feature Messages project key is required. Get yours at https://your-saas-dashboard.com');
11
+ }
12
+ // Validate that the service is properly configured
13
+ if (process.env.REACT_APP_SUPABASE_URL === 'https://your-saas-project.supabase.co' ||
14
+ process.env.REACT_APP_SUPABASE_KEY === 'your-readonly-anon-key') {
15
+ throw new Error('Feature Messages service is not properly configured. ' +
16
+ 'Please contact support@your-saas.com for assistance.');
17
+ }
18
+ customerConfig = config;
19
+ if (!featureMessagesClient) {
20
+ featureMessagesClient = createClient(supabaseUrl, supabaseKey, {
10
21
  global: {
11
22
  headers: {
12
- "X-Feature-Messages": "react-notified", // debugging / tracing
23
+ // Pass the project key in headers for filtering
24
+ 'x-feature-messages-project-key': config.projectKey,
25
+ // Add package version for analytics/debugging
26
+ 'x-feature-messages-version': process.env.npm_package_version || '1.0.0',
27
+ // Identify this as package usage (not dashboard)
28
+ 'x-feature-messages-source': 'npm-package',
29
+ },
30
+ },
31
+ realtime: {
32
+ headers: {
33
+ 'x-feature-messages-project-key': config.projectKey,
34
+ 'x-feature-messages-source': 'npm-package',
13
35
  },
14
36
  },
15
37
  });
16
38
  }
17
- return client;
39
+ return featureMessagesClient;
40
+ };
41
+ /**
42
+ * Get the initialized feature messages client
43
+ * Used internally by the package components and hooks
44
+ */
45
+ export const getFeatureMessagesClient = () => {
46
+ if (!featureMessagesClient || !customerConfig) {
47
+ throw new Error('Feature Messages not initialized. Make sure you have wrapped your app with FeatureMessagesProvider.\n' +
48
+ 'Get your project key at https://your-saas-dashboard.com');
49
+ }
50
+ return featureMessagesClient;
51
+ };
52
+ /**
53
+ * Get the current customer configuration
54
+ * Used internally by the package
55
+ */
56
+ export const getCustomerConfig = () => {
57
+ return customerConfig;
58
+ };
59
+ /**
60
+ * Reset the client (useful for testing or switching configurations)
61
+ */
62
+ export const resetFeatureMessagesClient = () => {
63
+ featureMessagesClient = null;
64
+ customerConfig = null;
18
65
  };
package/dist/types.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- export type NotifyMessage = {
1
+ import { ReactNode } from "react";
2
+ import { FeatureMessagesConfig } from "./supabase/supabaseClient";
3
+ export type FeatureMessage = {
2
4
  id: string;
3
5
  project_key: string;
4
6
  route: string;
5
7
  title: string;
6
8
  content: string;
7
- position: string;
9
+ position: 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
8
10
  positionValue: number;
9
11
  backgroundColor: string;
10
12
  borderColor: string;
@@ -13,8 +15,16 @@ export type NotifyMessage = {
13
15
  borderWidth: number;
14
16
  is_active: boolean;
15
17
  created_at: string;
16
- type: string;
18
+ type?: 'Information' | 'Warning' | 'Error';
17
19
  };
18
- export type NotifyMessageProviderProps = {
20
+ export interface FeatureMessageFilter {
19
21
  projectKey: string;
22
+ route: string;
23
+ deviceType?: 'desktop' | 'tablet' | 'mobile';
24
+ }
25
+ export type FeatureMessageProviderProps = {
26
+ config: FeatureMessagesConfig | null;
27
+ projectKey: string;
28
+ children: ReactNode;
29
+ endpoint: string;
20
30
  };
@@ -0,0 +1,17 @@
1
+ import { FeatureMessage, FeatureMessageFilter } from './types';
2
+ export interface UseFeatureMessagesReturn {
3
+ message: FeatureMessage | null;
4
+ messages: FeatureMessage[];
5
+ loading: boolean;
6
+ error: string | null;
7
+ dismissMessage: (messageId: string) => void;
8
+ refetch: () => Promise<void>;
9
+ }
10
+ export declare const useFeatureMessages: (filter: FeatureMessageFilter) => {
11
+ message: FeatureMessage | null;
12
+ messages: FeatureMessage[];
13
+ loading: boolean;
14
+ error: string | null;
15
+ dismissMessage: (messageId: string) => void;
16
+ refetch: () => Promise<void>;
17
+ };
@@ -0,0 +1,148 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { getFeatureMessagesClient } from './supabase/supabaseClient';
3
+ import { getCustomerConfig } from './supabase/supabaseClient';
4
+ import useDeviceDetection from './UseDeviceDetection';
5
+ const routeMatches = (pattern, path) => {
6
+ if (pattern === path)
7
+ return true;
8
+ const escapeRegex = (str) => str.replace(/([.+^=!:${}()|[\]/\\])/g, "\\$1");
9
+ const regexPattern = "^" + pattern.split("*").map(escapeRegex).join(".*") + "$";
10
+ try {
11
+ return new RegExp(regexPattern).test(path);
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ };
17
+ export const useFeatureMessages = (filter) => {
18
+ const [messages, setMessages] = useState([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [error, setError] = useState(null);
21
+ const [dismissedMessages, setDismissedMessages] = useState(new Set());
22
+ const channelRef = useRef(null);
23
+ const deviceInfo = useDeviceDetection();
24
+ const customerConfig = getCustomerConfig();
25
+ const { projectKey, route, deviceType } = filter;
26
+ const currentDeviceType = deviceType || deviceInfo;
27
+ // Filter messages based on route and active status
28
+ const filterMessages = useCallback((allMessages) => {
29
+ const now = new Date();
30
+ return allMessages.filter(message => {
31
+ // Check if message is active
32
+ if (!message.is_active)
33
+ return false;
34
+ // Check if message has ended
35
+ //if (message.ended_at && new Date(message.ended_at) < now) return false;
36
+ // Check project key (this should match what the customer set up in the dashboard)
37
+ if (message.project_key !== projectKey)
38
+ return false;
39
+ // Check route matching (including wildcards)
40
+ if (!routeMatches(route, message.route))
41
+ return false;
42
+ // Check if message has been dismissed
43
+ if (dismissedMessages.has(message.id))
44
+ return false;
45
+ return true;
46
+ });
47
+ }, [projectKey, route, dismissedMessages]);
48
+ // Get the most recent active message (since trigger enforces single active message)
49
+ const getActiveMessage = useCallback((filteredMessages) => {
50
+ if (filteredMessages.length === 0)
51
+ return null;
52
+ // Sort by creation date (newest first) since only one can be active
53
+ const sorted = [...filteredMessages].sort((a, b) => {
54
+ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
55
+ });
56
+ return sorted[0];
57
+ }, []);
58
+ // Fetch messages from your SaaS backend
59
+ const fetchMessages = useCallback(async () => {
60
+ try {
61
+ setError(null);
62
+ const client = getFeatureMessagesClient();
63
+ // Query messages for this project key
64
+ const { data, error: fetchError } = await client
65
+ .from('feature_messages')
66
+ .select('*')
67
+ .eq('project_key', projectKey)
68
+ .eq('is_active', true);
69
+ if (fetchError) {
70
+ throw fetchError;
71
+ }
72
+ setMessages(data || []);
73
+ }
74
+ catch (err) {
75
+ console.error('Error fetching feature messages:', err);
76
+ setError(err instanceof Error ? err.message : 'Failed to fetch messages');
77
+ }
78
+ finally {
79
+ setLoading(false);
80
+ }
81
+ }, [projectKey]);
82
+ // Set up realtime subscription
83
+ useEffect(() => {
84
+ try {
85
+ const client = getFeatureMessagesClient();
86
+ // Clean up existing subscription
87
+ if (channelRef.current) {
88
+ client.removeChannel(channelRef.current);
89
+ }
90
+ // Create new subscription for this project
91
+ const channel = client
92
+ .channel(`feature_messages_${projectKey}`)
93
+ .on('postgres_changes', {
94
+ event: '*',
95
+ schema: 'public',
96
+ table: 'feature_messages',
97
+ filter: `project_key=eq.${projectKey}`,
98
+ }, (payload) => {
99
+ if (customerConfig?.debug) {
100
+ console.log('Feature message change received:', payload);
101
+ }
102
+ if (payload.eventType === 'INSERT' || payload.eventType === 'UPDATE') {
103
+ const newMessage = payload.new;
104
+ setMessages(prev => {
105
+ const existing = prev.find(m => m.id === newMessage.id);
106
+ if (existing) {
107
+ return prev.map(m => m.id === newMessage.id ? newMessage : m);
108
+ }
109
+ return [...prev, newMessage];
110
+ });
111
+ }
112
+ else if (payload.eventType === 'DELETE') {
113
+ const deletedMessage = payload.old;
114
+ setMessages(prev => prev.filter(m => m.id !== deletedMessage.id));
115
+ }
116
+ })
117
+ .subscribe();
118
+ channelRef.current = channel;
119
+ // Initial fetch
120
+ fetchMessages();
121
+ return () => {
122
+ if (channelRef.current) {
123
+ client.removeChannel(channelRef.current);
124
+ channelRef.current = null;
125
+ }
126
+ };
127
+ }
128
+ catch (err) {
129
+ setError(err instanceof Error ? err.message : 'Failed to initialize feature messages');
130
+ setLoading(false);
131
+ }
132
+ }, [projectKey, fetchMessages, customerConfig?.debug]);
133
+ // Dismiss a message
134
+ const dismissMessage = useCallback((messageId) => {
135
+ setDismissedMessages(prev => new Set([...prev, messageId]));
136
+ }, []);
137
+ // Get filtered messages and active message
138
+ const filteredMessages = filterMessages(messages);
139
+ const activeMessage = getActiveMessage(filteredMessages);
140
+ return {
141
+ message: activeMessage,
142
+ messages: filteredMessages,
143
+ loading,
144
+ error,
145
+ dismissMessage,
146
+ refetch: fetchMessages,
147
+ };
148
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-notify-sdk",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "description": "SDK for displaying real-time route-specific messages in React apps",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",