react-notify-sdk 1.0.30 → 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.
- package/dist/FadeWrapper.d.ts +2 -2
- package/dist/NotifyMessage.d.ts +2 -2
- package/dist/NotifyMessage.js +29 -26
- package/dist/NotifyProvider.d.ts +12 -2
- package/dist/NotifyProvider.js +65 -69
- package/dist/UseDeviceDetection.js +3 -3
- package/dist/supabase/supabaseClient.d.ts +32 -1
- package/dist/supabase/supabaseClient.js +63 -3
- package/dist/types.d.ts +14 -4
- package/dist/useFeatureMessage.d.ts +17 -0
- package/dist/useFeatureMessage.js +148 -0
- package/package.json +1 -1
package/dist/FadeWrapper.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { FeatureMessage } from './types';
|
|
3
3
|
interface FadeWrapperProps {
|
|
4
4
|
visible: boolean;
|
|
5
|
-
message:
|
|
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>>;
|
package/dist/NotifyMessage.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FeatureMessage } from './types';
|
|
2
2
|
interface NotifyMessageProps {
|
|
3
|
-
message:
|
|
3
|
+
message: FeatureMessage;
|
|
4
4
|
}
|
|
5
5
|
declare const NotificationMessage: ({ message }: NotifyMessageProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
export default NotificationMessage;
|
package/dist/NotifyMessage.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
package/dist/NotifyProvider.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {};
|
package/dist/NotifyProvider.js
CHANGED
|
@@ -1,82 +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
|
-
import {
|
|
6
|
+
import { getSupabaseClient } from './supabase/supabaseClient';
|
|
7
|
+
import { useFeatureMessages } from './useFeatureMessage';
|
|
7
8
|
import useDeviceDetection from './UseDeviceDetection';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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();
|
|
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);
|
|
31
41
|
useEffect(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
// Trigger fade out before removing message
|
|
51
|
-
setVisible(false);
|
|
52
|
-
setTimeout(() => setMessage(null), 300); // match fadeOut duration
|
|
53
|
-
}
|
|
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);
|
|
54
58
|
};
|
|
55
|
-
fetchMessage();
|
|
56
|
-
const subscription = supabase
|
|
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, location.pathname)) {
|
|
65
|
-
setMessage(msg);
|
|
66
|
-
setVisible(true);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
setVisible(false);
|
|
70
|
-
setTimeout(() => setMessage(null), 300);
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
.subscribe();
|
|
74
59
|
return () => {
|
|
75
|
-
|
|
60
|
+
window.removeEventListener('popstate', handleRouteChange);
|
|
61
|
+
history.pushState = originalPushState;
|
|
62
|
+
history.replaceState = originalReplaceState;
|
|
76
63
|
};
|
|
77
|
-
}, [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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);
|
|
82
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.
|
|
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 +1,32 @@
|
|
|
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
|
+
/**
|
|
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,5 +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
|
-
export const supabase = createClient(supabaseUrl, supabaseKey)
|
|
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://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, {
|
|
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
|
+
};
|
|
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;
|
|
65
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
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:
|
|
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
|
|
18
|
+
type?: 'Information' | 'Warning' | 'Error';
|
|
17
19
|
};
|
|
18
|
-
export
|
|
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
|
+
};
|