react-notify-sdk 1.0.34 → 1.0.35

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 { FeatureMessage } from './types';
2
+ import { FeatureMessages } from './types';
3
3
  interface FadeWrapperProps {
4
4
  visible: boolean;
5
- message: FeatureMessage;
5
+ message: FeatureMessages;
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>>;
@@ -0,0 +1,6 @@
1
+ import { FeatureMessages } from './types';
2
+ interface FeatureMessageProps {
3
+ message: FeatureMessages;
4
+ }
5
+ declare const FeatureMessage: ({ message }: FeatureMessageProps) => import("react/jsx-runtime").JSX.Element;
6
+ export default FeatureMessage;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /////////////---Icon imports---////////////////////
3
+ import { AlertTriangle, CircleX, Info } from "lucide-react";
4
+ const FeatureMessage = ({ message }) => {
5
+ function hexToRgba(hex, alpha) {
6
+ // Remove leading #
7
+ hex = hex.replace(/^#/, "");
8
+ // Expand shorthand form (#03F → #0033FF)
9
+ if (hex.length === 3) {
10
+ hex = hex.split("").map(x => x + x).join("");
11
+ }
12
+ // Parse into r, g, b
13
+ const bigint = parseInt(hex, 16);
14
+ const r = (bigint >> 16) & 255;
15
+ const g = (bigint >> 8) & 255;
16
+ const b = bigint & 255;
17
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
18
+ }
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
+ border: `1px solid ${message?.textColor}`, // Completed border style
27
+ borderRadius: "8px",
28
+ borderWidth: "1px",
29
+ borderColor: `${hexToRgba(message.borderColor, 0.3)}`,
30
+ boxShadow: `0 10px 15px -3px ${hexToRgba(message.borderColor, 0.4)}, 0 4px 6px -4px ${hexToRgba(message.borderColor, 0.4)}`
31
+ }, children: [_jsxs("div", { className: 'w-full flex flex-row items-center', style: {
32
+ width: '100%',
33
+ display: 'flex',
34
+ flexDirection: 'row',
35
+ alignItems: 'center'
36
+ }, 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: {
37
+ fontSize: '1.125rem', // text-lg = 18px = 1.125rem
38
+ fontWeight: 600, // font-semibold = 600
39
+ color: `${message?.textColor}`,
40
+ marginLeft: '12px'
41
+ }, children: message.title })] }), _jsx("p", { className: `text-sm text-[${message?.textColor}]`, style: {
42
+ marginTop: '8px',
43
+ fontSize: '0.875rem', // text-sm = 14px = 0.875rem
44
+ color: `${message?.textColor}`,
45
+ }, children: message.content })] }));
46
+ };
47
+ export default FeatureMessage;
@@ -0,0 +1,2 @@
1
+ import { FeatureMessageProviderProps } from './types';
2
+ export declare const FeatureMessageProvider: ({ projectKey, }: FeatureMessageProviderProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // src/FeatureMessageProvider.tsx
3
+ import { useEffect, useState } from 'react';
4
+ import FeatureMessage from './FeatureMessage';
5
+ import { getSupabaseClient } from './supabase/supabaseClient';
6
+ import useDeviceDetection from './useDeviceDetection';
7
+ import FadeWrapper from './FadeWrapper';
8
+ // Wildcard route matching utility
9
+ const routeMatches = (pattern, path) => {
10
+ if (pattern === path)
11
+ return true;
12
+ const escapeRegex = (str) => str.replace(/([.+^=!:${}()|[\]/\\])/g, "\\$1");
13
+ const regexPattern = "^" + pattern.split("*").map(escapeRegex).join(".*") + "$";
14
+ try {
15
+ return new RegExp(regexPattern).test(path);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ };
21
+ export const FeatureMessageProvider = ({ projectKey,
22
+ //className,
23
+ //style,
24
+ //disableDefaultStyles = true,
25
+ }) => {
26
+ const [message, setMessage] = useState(null);
27
+ const [visible, setVisible] = useState(false);
28
+ const device = useDeviceDetection();
29
+ // Create supabase client with project key header
30
+ const supabaseClient = getSupabaseClient();
31
+ useEffect(() => {
32
+ if (typeof window === 'undefined')
33
+ return; // Skip on server
34
+ const fetchMessage = async () => {
35
+ const { data } = await supabaseClient
36
+ .from('feature_messages')
37
+ .select('*')
38
+ .eq('project_key', projectKey)
39
+ .eq('route', window.location.pathname)
40
+ .eq('is_active', true)
41
+ .order('created_at', { ascending: false });
42
+ //.limit(1)
43
+ //.single();
44
+ const match = data?.find((msg) => routeMatches(msg.route, window.location.pathname));
45
+ if (match) {
46
+ setMessage(match);
47
+ setVisible(true); // Fade in
48
+ }
49
+ else {
50
+ // Trigger fade out before removing message
51
+ setVisible(false);
52
+ setTimeout(() => setMessage(null), 300); // match fadeOut duration
53
+ }
54
+ };
55
+ fetchMessage();
56
+ const subscription = supabaseClient
57
+ .channel('feature_messages_channel')
58
+ .on('postgres_changes', {
59
+ event: '*',
60
+ schema: 'public',
61
+ table: 'feature_messages',
62
+ }, (payload) => {
63
+ const msg = payload.new;
64
+ if (msg.project_key === projectKey && msg.is_active && routeMatches(msg.route, window.location.pathname)) {
65
+ setMessage(msg);
66
+ setVisible(true);
67
+ }
68
+ else {
69
+ setVisible(false);
70
+ setTimeout(() => setMessage(null), 300);
71
+ }
72
+ })
73
+ .subscribe();
74
+ return () => {
75
+ supabaseClient.removeChannel(subscription);
76
+ };
77
+ }, [window.location.pathname, projectKey]);
78
+ if (!message)
79
+ return null;
80
+ //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`;
81
+ return (_jsx(FadeWrapper, { visible: visible, device: device, message: message, children: _jsx(FeatureMessage, { message: message }) }));
82
+ };
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { NotifyProvider } from './NotifyProvider';
1
+ export { FeatureMessageProvider } from './FeatureMessageProvider';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { setup } from 'goober';
2
2
  import { createElement } from 'react';
3
3
  setup(createElement);
4
- export { NotifyProvider } from './NotifyProvider';
4
+ export { FeatureMessageProvider } from './FeatureMessageProvider';
@@ -1,32 +1,12 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
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
2
  /**
20
- * Get the initialized feature messages client
21
- * Used internally by the package components and hooks
3
+ * Get the Supabase client for the SDK
4
+ * This connects to YOUR centralized notification platform database
5
+ * No configuration needed from developers - it's plug and play!
22
6
  */
23
- export declare const getFeatureMessagesClient: () => SupabaseClient;
7
+ export declare function getSupabaseClient(): SupabaseClient;
24
8
  /**
25
- * Get the current customer configuration
26
- * Used internally by the package
9
+ * Optional: Allow overriding the default configuration for testing/development
10
+ * This is mainly for your internal use during SDK development
27
11
  */
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;
12
+ export declare function overrideSupabaseConfig(url: string, anonKey: string): void;
@@ -1,65 +1,23 @@
1
1
  import { createClient } from '@supabase/supabase-js';
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
- // Create a single supabase client for interacting with your database
5
- //export const supabase = 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://dhgnstjrkeuqnsapwcec.supabase.co' ||
14
- process.env.REACT_APP_SUPABASE_KEY === 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRoZ25zdGpya2V1cW5zYXB3Y2VjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTA2MDA0NTEsImV4cCI6MjA2NjE3NjQ1MX0.TLBAPIWjPYvzNUUPHyakIypRydAeWM8Y1OrUzVvIgLQ') {
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, {
21
- global: {
22
- headers: {
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',
35
- },
36
- },
37
- });
38
- }
39
- return featureMessagesClient;
40
- };
2
+ // Your centralized Supabase configuration (hardcoded in the SDK)
3
+ const SDK_SUPABASE_URL = 'https://dhgnstjrkeuqnsapwcec.supabase.co';
4
+ const SDK_SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRoZ25zdGpya2V1cW5zYXB3Y2VjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTA2MDA0NTEsImV4cCI6MjA2NjE3NjQ1MX0.TLBAPIWjPYvzNUUPHyakIypRydAeWM8Y1OrUzVvIgLQ';
5
+ let supabaseClient = null;
41
6
  /**
42
- * Get the initialized feature messages client
43
- * Used internally by the package components and hooks
7
+ * Get the Supabase client for the SDK
8
+ * This connects to YOUR centralized notification platform database
9
+ * No configuration needed from developers - it's plug and play!
44
10
  */
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');
11
+ export function getSupabaseClient() {
12
+ if (!supabaseClient) {
13
+ supabaseClient = createClient(SDK_SUPABASE_URL, SDK_SUPABASE_ANON_KEY);
49
14
  }
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
- };
15
+ return supabaseClient;
16
+ }
59
17
  /**
60
- * Reset the client (useful for testing or switching configurations)
18
+ * Optional: Allow overriding the default configuration for testing/development
19
+ * This is mainly for your internal use during SDK development
61
20
  */
62
- export const resetFeatureMessagesClient = () => {
63
- featureMessagesClient = null;
64
- customerConfig = null;
65
- };
21
+ export function overrideSupabaseConfig(url, anonKey) {
22
+ supabaseClient = createClient(url, anonKey);
23
+ }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- import { ReactNode } from "react";
2
- import { FeatureMessagesConfig } from "./supabase/supabaseClient";
3
- export type FeatureMessage = {
1
+ export type FeatureMessages = {
4
2
  id: string;
5
3
  project_key: string;
6
4
  route: string;
@@ -17,14 +15,8 @@ export type FeatureMessage = {
17
15
  created_at: string;
18
16
  type?: 'Information' | 'Warning' | 'Error';
19
17
  };
20
- export interface FeatureMessageFilter {
21
- projectKey: string;
22
- route: string;
23
- deviceType?: 'desktop' | 'tablet' | 'mobile';
24
- }
25
18
  export type FeatureMessageProviderProps = {
26
- config: FeatureMessagesConfig | null;
27
19
  projectKey: string;
28
- children: ReactNode;
29
- endpoint: string;
20
+ className?: string;
21
+ disableDefaultStyles?: boolean;
30
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-notify-sdk",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
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",
@@ -22,13 +22,14 @@
22
22
  "ua-parser-js": "^2.0.4"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/react": "^19.1.8",
25
+ "@types/react": "^19.1.13",
26
+ "@types/react-dom": "^19.1.9",
26
27
  "@types/react-router-dom": "^5.3.3",
27
- "typescript": "^5.8.3"
28
+ "typescript": "^5.9.2"
28
29
  },
29
30
  "peerDependencies": {
30
- "react": "^19.1.0",
31
- "react-dom": "^19.0.0",
31
+ "react": "^19.1.1",
32
+ "react-dom": "^19.1.1",
32
33
  "react-router-dom": "^7.6.2"
33
34
  }
34
35
  }
@@ -1,6 +0,0 @@
1
- import { FeatureMessage } from './types';
2
- interface NotifyMessageProps {
3
- message: FeatureMessage;
4
- }
5
- declare const NotificationMessage: ({ message }: NotifyMessageProps) => import("react/jsx-runtime").JSX.Element;
6
- export default NotificationMessage;
@@ -1,49 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import FadeWrapper from './FadeWrapper';
3
- /////////////---Icon imports---////////////////////
4
- import { AlertTriangle, CircleX, Info } from "lucide-react";
5
- import useDeviceDetection from './UseDeviceDetection';
6
- const NotificationMessage = ({ message }) => {
7
- const device = useDeviceDetection();
8
- function hexToRgba(hex, alpha) {
9
- // Remove leading #
10
- hex = hex.replace(/^#/, "");
11
- // Expand shorthand form (#03F → #0033FF)
12
- if (hex.length === 3) {
13
- hex = hex.split("").map(x => x + x).join("");
14
- }
15
- // Parse into r, g, b
16
- const bigint = parseInt(hex, 16);
17
- const r = (bigint >> 16) & 255;
18
- const g = (bigint >> 8) & 255;
19
- const b = bigint & 255;
20
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
21
- }
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 })] }) }));
48
- };
49
- export default NotificationMessage;
@@ -1,12 +0,0 @@
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,76 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // src/FeatureMessage.tsx
3
- import { useEffect, useState, createContext, useContext } from 'react';
4
- import NotificationMessage from './NotifyMessage';
5
- import { getSupabaseClient } from './supabase/supabaseClient';
6
- import { useFeatureMessages } from './useFeatureMessage';
7
- import useDeviceDetection from './UseDeviceDetection';
8
- const FeatureMessagesContext = createContext({
9
- config: null,
10
- projectKey: null,
11
- });
12
- export const NotifyProvider = ({ children, projectKey, endpoint, }) => {
13
- const config = { projectKey, endpoint };
14
- useEffect(() => {
15
- getSupabaseClient(config);
16
- }, [projectKey, endpoint]);
17
- const [message, setMessage] = useState(null);
18
- const [visible, setVisible] = useState(false);
19
- const device = useDeviceDetection();
20
- if (!message)
21
- return null;
22
- return (_jsxs(FeatureMessagesContext.Provider, { value: { config, projectKey }, children: [children, _jsx(AutomaticFeatureMessages, { projectKey: projectKey })] }));
23
- /*
24
- return message ? (
25
- <FadeWrapper visible={visible} device={device} message={message}>
26
- <NotificationMessage message={message} />
27
- </FadeWrapper>
28
- )
29
- :
30
- null;
31
- */
32
- };
33
- /**
34
- * Internal component that automatically displays messages based on current route
35
- */
36
- const AutomaticFeatureMessages = ({ projectKey }) => {
37
- // Get current route - works with any router or vanilla JavaScript
38
- const [currentRoute, setCurrentRoute] = useState(window.location.pathname);
39
- useEffect(() => {
40
- // Listen for route changes (works with React Router, Next.js Router, etc.)
41
- const handleRouteChange = () => {
42
- setCurrentRoute(window.location.pathname);
43
- };
44
- // Listen for browser navigation
45
- window.addEventListener('popstate', handleRouteChange);
46
- // Listen for programmatic navigation (works with most routers)
47
- const originalPushState = history.pushState;
48
- const originalReplaceState = history.replaceState;
49
- history.pushState = function () {
50
- originalPushState.apply(history, arguments);
51
- setTimeout(handleRouteChange, 0);
52
- };
53
- history.replaceState = function () {
54
- originalReplaceState.apply(history, arguments);
55
- setTimeout(handleRouteChange, 0);
56
- };
57
- return () => {
58
- window.removeEventListener('popstate', handleRouteChange);
59
- history.pushState = originalPushState;
60
- history.replaceState = originalReplaceState;
61
- };
62
- }, []);
63
- // Use the internal hook to get messages for current route
64
- const { message, dismissMessage } = useFeatureMessages({
65
- projectKey,
66
- route: currentRoute,
67
- });
68
- // Automatically render the message if one exists
69
- return message ? (_jsx(NotificationMessage, { message: message })) : null;
70
- };
71
- /**
72
- * Hook to get the feature messages context (for advanced use cases)
73
- */
74
- export const useFeatureMessagesContext = () => {
75
- return useContext(FeatureMessagesContext);
76
- };
@@ -1,17 +0,0 @@
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
- };
@@ -1,148 +0,0 @@
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
- };