From 780b62fd2f052e90a75315e42cbbeebaabc120f0 Mon Sep 17 00:00:00 2001 From: millerm30 Date: Tue, 15 Sep 2026 17:20:29 -0230 Subject: [PATCH 1/6] Drop Slack and web-push UI so in-app and prefs go through the CDK js-core client. Do not publish until @notificationapi/core with the CDK paths is released and this package is bumped to it. Co-authored-by: Cursor --- .../Notifications/NotificationFeed.tsx | 26 - .../Notifications/NotificationLauncher.tsx | 23 +- .../Notifications/NotificationPopup.tsx | 12 +- .../NotificationPreferencesInline.tsx | 26 +- .../NotificationPreferencesPopup.tsx | 9 - .../Preferences/PreferenceInput.tsx | 4 + lib/components/Provider/context.ts | 5 - lib/components/Provider/index.tsx | 218 +------ lib/components/Slack/SlackConnect.tsx | 534 ------------------ lib/components/Slack/index.tsx | 1 - .../WebPush/WebPushOptInMessage.tsx | 110 ---- lib/main.ts | 1 - src/LiveComponents.tsx | 6 +- src/MockedComponents.tsx | 1 - 14 files changed, 16 insertions(+), 960 deletions(-) delete mode 100644 lib/components/Slack/SlackConnect.tsx delete mode 100644 lib/components/Slack/index.tsx delete mode 100644 lib/components/WebPush/WebPushOptInMessage.tsx diff --git a/lib/components/Notifications/NotificationFeed.tsx b/lib/components/Notifications/NotificationFeed.tsx index 37989c2..3e32057 100644 --- a/lib/components/Notifications/NotificationFeed.tsx +++ b/lib/components/Notifications/NotificationFeed.tsx @@ -6,10 +6,7 @@ import { InboxHeaderProps } from './InboxHeader'; import { NotificationPreferencesPopup } from '../Preferences'; import { InAppNotification } from '@notificationapi/core/dist/interfaces'; import { Filter, Pagination } from './interface'; -import Divider from '@mui/material/Divider'; import { useTheme } from '@mui/material/styles'; -import WebPushOptInMessage from '../WebPush/WebPushOptInMessage'; -import Language from '@mui/icons-material/Language'; import { getThemeColors } from '../../utils/theme'; export type NotificationFeedProps = { @@ -95,29 +92,6 @@ export const NotificationFeed: React.FC = (props) => { imageShape={config.imageShape} newTab={config.newTab} /> - {context.webPushOptInMessage && - localStorage.getItem('hideWebPushOptInMessage') !== 'true' && ( -
- - } - descriptionStyle={{ - flexDirection: 'column', // Stack the elements vertically - justifyContent: 'flex-start', // Align items to the left - fontSize: '14px', - alignItems: 'flex-start' // Align items to the left - }} - buttonContainerStyle={{ - justifyContent: 'flex-start', // Align buttons to the left - alignItems: 'flex-start', // Align buttons to the left - marginTop: '10px' // Add some space between message and buttons - }} - /> -
- )} setOpenPreferences(false)} diff --git a/lib/components/Notifications/NotificationLauncher.tsx b/lib/components/Notifications/NotificationLauncher.tsx index f5ccfdd..61f8a11 100644 --- a/lib/components/Notifications/NotificationLauncher.tsx +++ b/lib/components/Notifications/NotificationLauncher.tsx @@ -5,9 +5,8 @@ import { NotificationPopupProps } from './NotificationPopup'; import { NotificationAPIContext } from '../Provider/context'; import { NotificationPreferencesPopup } from '../Preferences'; import { Position } from './interface'; -import { LanguageOutlined, NotificationsOutlined } from '@mui/icons-material'; -import { Divider, IconButton, Popover, useTheme } from '@mui/material'; -import WebPushOptInMessage from '../WebPush/WebPushOptInMessage'; +import { NotificationsOutlined } from '@mui/icons-material'; +import { IconButton, Popover, useTheme } from '@mui/material'; import { getThemeColors } from '../../utils/theme'; type NotificationLaucherProps = NotificationPopupProps & { @@ -149,24 +148,6 @@ export const NotificationLauncher: React.FC = ( header={config.header} newTab={config.newTab} /> - {context.webPushOptInMessage && - localStorage.getItem('hideWebPushOptInMessage') !== 'true' && ( -
- - - } - alertContainerStyle={{ maxWidth: '345px' }} - /> -
- )} = (props) => { header={config.header} newTab={config.newTab} /> - {context.webPushOptInMessage && - localStorage.getItem('hideWebPushOptInMessage') !== 'true' && ( -
- - -
- )} - {' '} - {context.webPushOptInMessage && ( -
- - -
- )} + ); } diff --git a/lib/components/Preferences/NotificationPreferencesPopup.tsx b/lib/components/Preferences/NotificationPreferencesPopup.tsx index 7c1d8b8..8d81113 100644 --- a/lib/components/Preferences/NotificationPreferencesPopup.tsx +++ b/lib/components/Preferences/NotificationPreferencesPopup.tsx @@ -2,7 +2,6 @@ import { useContext } from 'react'; import { Preferences } from './Preferences'; import { NotificationAPIContext } from '../Provider/context'; import { Dialog, DialogTitle, DialogContent, useTheme } from '@mui/material'; -import WebPushOptInMessage from '../WebPush/WebPushOptInMessage'; import { getThemeColors } from '../../utils/theme'; type NotificationPreferencesPopupProps = { @@ -50,14 +49,6 @@ export function NotificationPreferencesPopup( sx={{ backgroundColor: themeColors.paper, color: themeColors.text }} > - {context.webPushOptInMessage && ( -
- -
- )} ); diff --git a/lib/components/Preferences/PreferenceInput.tsx b/lib/components/Preferences/PreferenceInput.tsx index 9431d11..cd48d46 100644 --- a/lib/components/Preferences/PreferenceInput.tsx +++ b/lib/components/Preferences/PreferenceInput.tsx @@ -80,6 +80,10 @@ export const PreferenceInput = ({ return ( <> {(notification.channels as Channels[]) + .filter( + (channel) => + channel !== Channels.WEB_PUSH && channel !== Channels.SLACK + ) .sort(sortChannels) .map((channel: Channels, i) => { const preference = preferences.find( diff --git a/lib/components/Provider/context.ts b/lib/components/Provider/context.ts index 83e5f33..2c7c732 100644 --- a/lib/components/Provider/context.ts +++ b/lib/components/Provider/context.ts @@ -14,7 +14,6 @@ export type Context = { notifications?: InAppNotification[]; preferences?: GetPreferencesResponse; userAccountMetaData?: { userAccountMetadata: UserAccountMetadata }; - webPushOptInMessage?: 'AUTOMATIC' | boolean; loadNotifications: (initial?: boolean) => void; markAsOpened: () => void; markAsArchived: (ids: string[] | 'ALL') => void; @@ -41,10 +40,6 @@ export type Context = { }[] ) => void; getClient: () => typeof NotificationAPIClientSDK; - setWebPushOptInMessage: React.Dispatch< - React.SetStateAction<'AUTOMATIC' | boolean> - >; - setWebPushOptIn: React.Dispatch>; }; export const NotificationAPIContext = createContext( diff --git a/lib/components/Provider/index.tsx b/lib/components/Provider/index.tsx index bdd525f..6fc9cd5 100644 --- a/lib/components/Provider/index.tsx +++ b/lib/components/Provider/index.tsx @@ -12,8 +12,7 @@ import { GetPreferencesResponse, InAppNotification, User, - UserAccountMetadata, - PushSubscription + UserAccountMetadata } from '@notificationapi/core/dist/interfaces'; import { BaseDeliveryOptions, @@ -48,16 +47,11 @@ type Props = ( playSoundOnNewNotification?: boolean; newNotificationSoundPath?: string; client?: typeof NotificationAPIClientSDK; - webPushOptInMessage?: 'AUTOMATIC' | boolean; - customServiceWorkerPath?: string; debug?: boolean; onNewNotifications?: (notifications: InAppNotification[]) => void; theme?: NotificationAPITheme; }; -// Ensure that the code runs only in the browser -const isClient = typeof window !== 'undefined'; - export const NotificationAPIProvider: React.FunctionComponent< PropsWithChildren > & { @@ -82,9 +76,7 @@ export const NotificationAPIProvider: React.FunctionComponent< initialLoadMaxAge: new Date(new Date().setMonth(new Date().getMonth() - 3)), playSoundOnNewNotification: false, newNotificationSoundPath: - 'https://proxy.notificationsounds.com/notification-sounds/elegant-notification-sound/download/file-sounds-1233-elegant.mp3', - webPushOptInMessage: 'AUTOMATIC' as 'AUTOMATIC' | boolean, - customServiceWorkerPath: '/notificationapi-service-worker.js' + 'https://proxy.notificationsounds.com/notification-sounds/elegant-notification-sound/download/file-sounds-1233-elegant.mp3' }; const config = { @@ -103,10 +95,6 @@ export const NotificationAPIProvider: React.FunctionComponent< const [loadingNotifications, setLoadingNotifications] = useState(false); const [oldestLoaded, setOldestLoaded] = useState(new Date().toISOString()); const [hasMore, setHasMore] = useState(true); - const [webPushOptInMessage, setWebPushOptInMessage] = useState< - 'AUTOMATIC' | boolean - >(config.webPushOptInMessage); - const [webPushOptIn, setWebPushOptIn] = useState(false); const playSound = useCallback(() => { if (config.playSoundOnNewNotification) { @@ -617,121 +605,6 @@ export const NotificationAPIProvider: React.FunctionComponent< } }; - /** - * Asks the user for permission to send web push notifications and subscribes to the push service if granted. - * - * @callback askForWebPushPermission - * - * @throws Will log an error code if the service worker registration or push subscription fails. - * - * Possible error codes: - * - `18`: The operation is insecure. This typically occurs if the code is run in an insecure context (e.g., not over HTTPS). - * - `19`: The operation is aborted. This can happen if the user denies the permission request. - * - `20`: The operation is invalid. This can occur if the provided application server key is invalid. - * - `21`: The operation is not allowed. This can happen if the user has blocked notifications for the site. - * - `22`: The operation is not supported. This can occur if the browser does not support the required features. - * - * @dependencies - * - `client`: The client instance used to identify the user with the web push tokens. - * - `config.customServiceWorkerPath`: The path to the custom service worker script. - * - `userAccountMetaData?.userAccountMetadata.environmentVapidPublicKey`: The VAPID public key for the environment. - */ - const askForWebPushPermission = useCallback((): void => { - debug.group('Requesting web push permission'); - debug.log('Service worker support check', { - supported: 'serviceWorker' in navigator, - customServiceWorkerPath: config.customServiceWorkerPath - }); - - if ('serviceWorker' in navigator) { - debug.log('Registering service worker'); - navigator.serviceWorker - .register(config.customServiceWorkerPath) - .then(async (registration) => { - debug.log('Service worker registered successfully'); - setWebPushOptInMessage(false); - requestNotificationPermission().then(async (permission) => { - debug.log('Notification permission result', { permission }); - if (permission === 'granted') { - debug.log('Permission granted, subscribing to push manager'); - await registration.pushManager - .subscribe({ - userVisibleOnly: true, - applicationServerKey: - userAccountMetaData?.userAccountMetadata - .environmentVapidPublicKey - }) - .then(async (res) => { - debug.log('Push subscription successful'); - const body = { - webPushTokens: [ - { - sub: { - endpoint: res.toJSON().endpoint as string, - keys: res.toJSON().keys as PushSubscription['keys'] - } - } - ] - }; - debug.log('Identifying user with web push tokens'); - await client.identify(body); - localStorage.setItem('hideWebPushOptInMessage', 'true'); - debug.log('Web push setup completed successfully'); - debug.groupEnd(); - }); - } else if (permission === 'denied') { - debug.warn('Permission for notifications was denied'); - console.log('Permission for notifications was denied'); - debug.groupEnd(); - } - }); - }) - .catch((e) => { - debug.error( - 'Service worker registration or push subscription failed', - e, - { - errorCode: e.code, - customServiceWorkerPath: config.customServiceWorkerPath - } - ); - - if (e.code === 18) { - console.error( - 'NotificationAPI guide: Probably you are not setup the service worker correctly. Please check the documentation at https://docs.notificationapi.com/guides/web-push#step-by-step-implementation Step 3: Service Worker Setup.' - ); - } else if (e.code === 19) { - console.error( - 'The operation is aborted. This can happen if the user denies the permission request.' - ); - } else if (e.code === 20) { - console.error( - 'The operation is invalid. This can occur if the provided application server key is invalid. Please contact NotificationAPI support.' - ); - } else if (e.code === 21) { - console.error( - 'The operation is not allowed. This can happen if the user has blocked notifications for the site. Please check your browser site settings Notifications part.' - ); - } else if (e.code === 22) { - console.error( - 'The operation is not supported. This can occur if the browser does not support the required features.' - ); - } else { - console.error(e); - } - debug.groupEnd(); - }); - } else { - debug.warn('Service worker not supported in this browser'); - debug.groupEnd(); - } - }, [ - client, - config.customServiceWorkerPath, - userAccountMetaData?.userAccountMetadata.environmentVapidPublicKey, - debug - ]); - useEffect(() => { debug.group('Provider initialization effect'); debug.log('Resetting state and loading initial data'); @@ -764,7 +637,7 @@ export const NotificationAPIProvider: React.FunctionComponent< debug.error('Failed to fetch initial preferences', error); debug.groupEnd(); }); - }, [client, loadNotifications, askForWebPushPermission, debug]); + }, [client, loadNotifications, debug]); useEffect(() => { debug.group('Fetching user account metadata'); @@ -772,36 +645,9 @@ export const NotificationAPIProvider: React.FunctionComponent< .getUserAccountMetadata() .then((res) => { debug.log('User account metadata loaded', { - hasWebPushEnabled: res.userAccountMetadata.hasWebPushEnabled, - environmentVapidPublicKey: res.userAccountMetadata - .environmentVapidPublicKey - ? 'present' - : 'missing' + hasLogo: !!res.userAccountMetadata?.logo }); setUserAccountMetaData(res); - if ( - isClient && - 'Notification' in window && - typeof Notification.requestPermission === 'function' - ) { - debug.log('Browser notification support detected', { - permission: Notification.permission - }); - if (Notification.permission !== 'default') { - debug.log( - 'Setting webPushOptInMessage to false (permission already set)' - ); - setWebPushOptInMessage(false); - } - } else { - debug.log( - 'Browser notification not supported, using server setting', - { - hasWebPushEnabled: res.userAccountMetadata.hasWebPushEnabled - } - ); - setWebPushOptInMessage(res.userAccountMetadata.hasWebPushEnabled); - } debug.groupEnd(); }) .catch((error) => { @@ -810,38 +656,10 @@ export const NotificationAPIProvider: React.FunctionComponent< }); }, [client, debug]); - useEffect(() => { - debug.group('Handling webPushOptInMessage state'); - debug.log('webPushOptInMessage value', webPushOptInMessage); - - if (webPushOptInMessage === 'AUTOMATIC') { - const hideMessage = - localStorage.getItem('hideWebPushOptInMessage') === 'true'; - debug.log('Automatic mode - checking localStorage', { - hideMessage, - shouldShow: !hideMessage - }); - setWebPushOptInMessage(!hideMessage); - } - debug.groupEnd(); - }, [webPushOptInMessage, debug]); - - useEffect(() => { - debug.group('Handling webPushOptIn state'); - debug.log('webPushOptIn state', webPushOptIn); - - if (webPushOptIn) { - debug.log('User opted in for web push, requesting permission'); - askForWebPushPermission(); - } - debug.groupEnd(); - }, [webPushOptIn, askForWebPushPermission, debug]); - const value: Context = { notifications, preferences, userAccountMetaData, - webPushOptInMessage, loadNotifications, markAsOpened, markAsArchived, @@ -849,17 +667,13 @@ export const NotificationAPIProvider: React.FunctionComponent< markAsClicked, updateDelivery, updateDeliveries, - getClient: () => client, - setWebPushOptInMessage, - setWebPushOptIn + getClient: () => client }; debug.log('NotificationAPI Provider rendering', { notificationsCount: notifications?.length || 0, hasPreferences: !!preferences, - hasUserAccountMetaData: !!userAccountMetaData, - webPushOptInMessage, - webPushOptIn + hasUserAccountMetaData: !!userAccountMetaData }); // Create MUI theme from theme prop @@ -887,23 +701,3 @@ const useNotificationAPIContext = (): Context => { return context; }; NotificationAPIProvider.useNotificationAPIContext = useNotificationAPIContext; - -const requestNotificationPermission = - async (): Promise => { - if ( - isClient && - 'Notification' in window && - typeof Notification.requestPermission === 'function' - ) { - try { - const permission = await Notification.requestPermission(); - return permission; - } catch (error) { - console.error('Error requesting notification permission:', error); - return 'default'; - } - } else { - console.warn('Web Push Notifications are not supported in this browser.'); - return 'default'; - } - }; diff --git a/lib/components/Slack/SlackConnect.tsx b/lib/components/Slack/SlackConnect.tsx deleted file mode 100644 index 4639af0..0000000 --- a/lib/components/Slack/SlackConnect.tsx +++ /dev/null @@ -1,534 +0,0 @@ -import { useContext, useEffect, useState, useCallback } from 'react'; -import { - Box, - Button, - Typography, - CircularProgress, - Alert, - Stack, - Autocomplete, - TextField, - Paper -} from '@mui/material'; -import { NotificationAPIContext } from '../Provider/context'; -import { User } from '@notificationapi/core/dist/interfaces'; - -interface SlackChannel { - id: string; - name: string; - type: 'channel' | 'user'; -} - -interface SlackConnectProps { - description?: string; - connectButtonText?: string; - editButtonText?: string; - disconnectButtonText?: string; - saveButtonText?: string; - cancelButtonText?: string; - connectedText?: string; - selectChannelText?: string; - /** - * Controls what type of Slack destination the user can select: - * - 'me': Only allows sending to the authenticated user's own DM (no channel picker or edit option) - * - 'any': Allows selecting any channel or user (default behavior) - */ - destinationType?: 'me' | 'any'; -} - -export function SlackConnect({ - description = 'Connect your Slack workspace to receive notifications directly in Slack.', - connectButtonText = 'Connect Slack', - editButtonText = 'Edit Channel', - disconnectButtonText = 'Disconnect', - saveButtonText = 'Save', - cancelButtonText = 'Cancel', - connectedText = 'Slack notifications will be sent to:', - selectChannelText = 'Choose a channel or user to receive notifications:', - destinationType = 'any' -}: SlackConnectProps = {}) { - const context = useContext(NotificationAPIContext); - const client = context?.getClient(); - const [slackToken, setSlackToken] = useState< - User['slackToken'] | undefined - >(); - const [slackChannel, setSlackChannel] = useState(); - const [channels, setChannels] = useState([]); - const [selectedChannel, setSelectedChannel] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [isEditing, setIsEditing] = useState(false); - const [hasMoreChannels, setHasMoreChannels] = useState(false); - const [hasMoreUsers, setHasMoreUsers] = useState(false); - - const fetchUserSlackStatus = useCallback(async () => { - if (!client) return; - - try { - setLoading(true); - - // Get user's current slack configuration using user.get - const user = await client.user.get(); - - if (user.slackToken) { - setSlackToken(user.slackToken); - } - - if (user.slackChannel) { - setSlackChannel(user.slackChannel); - } - } catch (err) { - console.error('Error fetching Slack status:', err); - // If the endpoint doesn't exist yet, that's okay - } finally { - setLoading(false); - } - }, [client]); - - const loadChannels = useCallback(async () => { - if (!client || !slackToken) return []; - - try { - setLoading(true); - setError(null); - - // Get channels and users from Slack - const response = await client.slack.getChannels(); - - // Combine channels and users into a single array - const allOptions: SlackChannel[] = [ - ...(response.channels || []) - .filter((c) => c.id && c.name) - .map((c) => ({ - id: c.id!, - name: c.name!, - type: 'channel' as SlackChannel['type'] - })), - ...(response.users || []) - .filter((u) => u.id && u.name) - .map((u) => ({ - id: u.id!, - name: u.name!, - type: 'user' as SlackChannel['type'] - })) - ]; - - setHasMoreChannels(response.hasMoreChannels || false); - setHasMoreUsers(response.hasMoreUsers || false); - - setChannels(allOptions); - return allOptions; - } catch (err) { - console.error('Error loading channels and users:', err); - setError('Failed to load Slack channels and users. Please try again.'); - return []; - } finally { - setLoading(false); - } - }, [client, slackToken]); - - useEffect(() => { - // Fetch the user's current slackToken and slackChannel from the API - fetchUserSlackStatus(); - }, [fetchUserSlackStatus]); - - useEffect(() => { - if (slackToken && !slackChannel && !isEditing) { - if (destinationType === 'me') { - // Auto-set DM for 'me' mode - const autoSetDirectMessage = async () => { - try { - setLoading(true); - setError(null); - - // Get the current Slack user's info - const response = await client?.slack.getChannels(); - - // Use the authenticated user from the response - const currentUser = response?.me; - - if (currentUser && currentUser.name && client) { - // Set the channel to the current user's DM - const formattedChannel = `@${currentUser.name}`; - await client.slack.setChannel(formattedChannel); - setSlackChannel(formattedChannel); - } else { - setError( - 'Unable to automatically set direct message. Please try again.' - ); - } - } catch (err) { - console.error('Error setting direct message:', err); - setError('Failed to set direct message. Please try again.'); - } finally { - setLoading(false); - } - }; - autoSetDirectMessage(); - } else { - // Load channels for 'any' mode - loadChannels(); - } - } - }, [ - slackToken, - slackChannel, - isEditing, - loadChannels, - destinationType, - client - ]); - - const handleConnectSlack = async () => { - if (!client) return; - - try { - setLoading(true); - setError(null); - - // Generate Slack OAuth URL - const url = await client.slack.getOAuthUrl(); - - // Redirect to Slack OAuth - window.location.href = url; - } catch (err) { - console.error('Error connecting to Slack:', err); - setError('Failed to connect to Slack. Please try again.'); - } finally { - setLoading(false); - } - }; - - const handleSaveChannel = async () => { - if (!client || !selectedChannel) return; - - try { - setLoading(true); - setError(null); - - let formattedChannel: string; - const trimmedValue = selectedChannel.trim(); - const isSlackId = /^[CDGUWTE][A-Z0-9]{8,}$/i.test(trimmedValue); - - // Check if selectedChannel is an ID or a name already in our list - const channelInfo = channels.find( - (c) => - c.id === trimmedValue || - c.name.toLowerCase() === trimmedValue.toLowerCase() - ); - - if (channelInfo) { - // If we found it in the list, use the formatted name (#channel or @user) - formattedChannel = `${channelInfo.type === 'channel' ? '#' : '@'}${channelInfo.name}`; - } else if ( - trimmedValue.startsWith('#') || - trimmedValue.startsWith('@') || - isSlackId - ) { - // User typed a custom value with proper prefix or a Slack ID - formattedChannel = trimmedValue; - } else { - // Invalid format - show error - setError( - 'Please enter a #channel-name, @username, or a Slack ID (e.g. C12345678)' - ); - setLoading(false); - return; - } - - // Set the selected channel with formatted name - await client.slack.setChannel(formattedChannel); - - setSlackChannel(formattedChannel); - setIsEditing(false); - setError(null); - } catch (err) { - console.error('Error saving channel:', err); - setError('Failed to save channel. Please try again.'); - } finally { - setLoading(false); - } - }; - - const handleDisconnect = async () => { - if (!client) return; - - try { - setLoading(true); - setError(null); - - // Remove slackToken and slackChannel using identify - await client.identify({ - // @ts-expect-error - null is not assignable to type string - slackToken: null, - // @ts-expect-error - null is not assignable to type string - slackChannel: null - }); - - setSlackToken(undefined); - setSlackChannel(undefined); - setSelectedChannel(''); - setChannels([]); - setIsEditing(false); - } catch (err) { - console.error('Error disconnecting Slack:', err); - setError('Failed to disconnect Slack. Please try again.'); - } finally { - setLoading(false); - } - }; - - const handleEdit = async () => { - setIsEditing(true); - - // Load channels if not already loaded - let channelsList = channels; - if (channels.length === 0) { - channelsList = await loadChannels(); - } - - // Parse the slackChannel to find the matching channel ID - if (slackChannel) { - const isSlackId = /^[CDGUWTE][A-Z0-9]{8,}$/i.test(slackChannel); - - if (isSlackId) { - setSelectedChannel(slackChannel); - } else if (slackChannel.startsWith('#') || slackChannel.startsWith('@')) { - const isChannel = slackChannel.startsWith('#'); - const channelName = slackChannel.substring(1); // Remove # or @ - const channelType = isChannel ? 'channel' : 'user'; - - // Find the channel ID that matches the name and type - const matchingChannel = channelsList.find( - (c) => c.name === channelName && c.type === channelType - ); - - if (matchingChannel) { - setSelectedChannel(matchingChannel.id); - } else { - // Channel not found in list (possibly due to pagination) - // Set the formatted channel string directly for freeSolo mode - setSelectedChannel(slackChannel); - } - } else { - setSelectedChannel(slackChannel); - } - } - }; - - const handleCancelEdit = () => { - setIsEditing(false); - setSelectedChannel(''); - }; - - if (!client) { - return null; - } - - // Show loading state - if (loading && !slackToken && !channels.length) { - return ( - - - - ); - } - - // No Slack token - show connect button - if (!slackToken) { - return ( - - {error && ( - - {error} - - )} - - - {description} - - - - - ); - } - - // For 'me' mode, show loading while auto-setting DM - if (destinationType === 'me' && slackToken && !slackChannel) { - return ( - - {error && ( - - {error} - - )} - - - - - ); - } - - // Has token but no channel (or editing) - // Don't show channel picker for 'me' mode - it auto-sets - if ((!slackChannel || isEditing) && destinationType === 'any') { - return ( - - {error && ( - - {error} - - )} - {loading ? ( - - - - ) : ( - - - {selectChannelText} - - ( - - {children && (hasMoreChannels || hasMoreUsers) && ( - - {hasMoreChannels && hasMoreUsers - ? 'Not all channels and users could be loaded.' - : hasMoreChannels - ? 'Not all channels could be loaded.' - : 'Not all users could be loaded.'}{' '} - Type #channel-name or @username to enter manually. - - )} - {children} - - ) - }} - options={channels.sort((a, b) => { - if (a.type === b.type) { - return a.name.localeCompare(b.name); - } - return a.type === 'channel' ? -1 : 1; - })} - groupBy={(option) => - typeof option === 'string' - ? '' - : option.type === 'channel' - ? 'Channels' - : 'Users' - } - getOptionLabel={(option) => - typeof option === 'string' - ? option - : `${option.type === 'channel' ? '#' : '@'}${option.name}` - } - sx={{ minWidth: 200, flexGrow: 1 }} - size="small" - value={ - channels.find((c) => c.id === selectedChannel) || - selectedChannel || - null - } - onChange={(_, newValue) => { - if (typeof newValue === 'string') { - setSelectedChannel(newValue); - } else if (newValue) { - setSelectedChannel(newValue.id); - } else { - setSelectedChannel(''); - } - }} - onInputChange={(_, newInputValue, reason) => { - if (reason === 'input') { - setSelectedChannel(newInputValue); - } - }} - isOptionEqualToValue={(option, value) => { - if (typeof option === 'string' || typeof value === 'string') { - return option === value; - } - return option.id === value.id; - }} - renderInput={(params) => ( - - )} - /> - - {isEditing && ( - - )} - - - )} - - ); - } - - // Has both token and channel - show connected state - return ( - - - {connectedText} - - - {slackChannel} - - {destinationType === 'any' && ( - - )} - - - ); -} diff --git a/lib/components/Slack/index.tsx b/lib/components/Slack/index.tsx deleted file mode 100644 index a5a6b8e..0000000 --- a/lib/components/Slack/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SlackConnect } from './SlackConnect'; diff --git a/lib/components/WebPush/WebPushOptInMessage.tsx b/lib/components/WebPush/WebPushOptInMessage.tsx deleted file mode 100644 index 82a3c40..0000000 --- a/lib/components/WebPush/WebPushOptInMessage.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useContext } from 'react'; -import { NotificationAPIContext } from '../Provider/context'; -import { Alert, Button } from '@mui/material'; -import LanguageOutlined from '@mui/icons-material/LanguageOutlined'; - -interface WebPushOptInMessageProps { - hideAfterInteraction?: boolean; - alertContainerStyle?: React.CSSProperties; - descriptionStyle?: React.CSSProperties; - buttonContainerStyle?: React.CSSProperties; - yesButtonStyle?: React.CSSProperties; - cancelButtonStyle?: React.CSSProperties; - description?: string; // Optional prop to override the alert description - icon?: React.ReactNode; // Optional prop to override the alert icon -} -const WebPushOptInMessage: React.FC = ({ - hideAfterInteraction: hideAfterInteraction, - alertContainerStyle: customAlertContainerStyle, - descriptionStyle: customDescriptionStyle, - buttonContainerStyle: customButtonContainerStyle, - yesButtonStyle: customYesButtonStyle, - cancelButtonStyle: customCancelButtonStyle, - description: customDescription, - icon: customIcons -}) => { - const context = useContext(NotificationAPIContext); - if (!context) { - return null; - } - - const alertContainerStyle = { - marginBottom: '10px', - borderRadius: '5px', - padding: '10px', - fontSize: '12px', // Smaller font size - margin: '10px auto', // Center if width is more than 500px - display: 'flex', - alignItems: 'center', // Align items vertically - ...customAlertContainerStyle - }; - - const descriptionStyle = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', // Align items vertically - fontSize: '12px', - width: '100%', - ...customDescriptionStyle - }; - - const buttonContainerStyle = { - display: 'flex', - alignItems: 'center', - ...customButtonContainerStyle - }; - - const yesButtonStyle = { - marginLeft: '10px', - marginRight: '10px', - fontSize: '10px', - ...customYesButtonStyle - }; - - const cancelButtonStyle = { - fontSize: '10px', - ...customCancelButtonStyle - }; - - return ( - } - style={alertContainerStyle} - > -
- - {customDescription ?? - `Would you like to enable web push notifications to stay updated?`} - -
- - -
-
-
- ); -}; - -export default WebPushOptInMessage; diff --git a/lib/main.ts b/lib/main.ts index 97fb151..a7dc5f8 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -14,7 +14,6 @@ export { NotificationPreferencesPopup } from './components/Preferences'; export { NotificationAPIProvider } from './components/Provider'; -export { SlackConnect } from './components/Slack'; // Debug utilities export { createDebugLogger, type DebugLogger } from './utils/debug'; diff --git a/src/LiveComponents.tsx b/src/LiveComponents.tsx index df74469..c5b602b 100644 --- a/src/LiveComponents.tsx +++ b/src/LiveComponents.tsx @@ -6,8 +6,7 @@ import { NotificationCounter, NotificationAPIProvider, NotificationPreferencesPopup, - NotificationPreferencesInline, - SlackConnect + NotificationPreferencesInline } from '../lib/main'; import { Button, @@ -144,9 +143,6 @@ const LiveComponents: React.FC = ({ playSoundOnNewNotification={true} debug={debugMode} > -

Slack Connect:

- -

Popup:

diff --git a/src/MockedComponents.tsx b/src/MockedComponents.tsx index 1f2dff1..adc27c9 100644 --- a/src/MockedComponents.tsx +++ b/src/MockedComponents.tsx @@ -29,7 +29,6 @@ const MockedComponents: React.FC = ({ <> Date: Wed, 16 Sep 2026 10:40:58 -0230 Subject: [PATCH 2/6] Increase global test timeout from 5 seconds to 15 seconds in Playwright configuration. --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index be6bd0b..5035b31 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Global timeout for each test - 5 seconds max */ - timeout: 5000, + timeout: 15000, /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ From 21345e2de431c582394582c24f5d67a4778757f8 Mon Sep 17 00:00:00 2001 From: millerm30 Date: Thu, 24 Sep 2026 08:09:01 -0230 Subject: [PATCH 3/6] update core sdk --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index defed04..782adf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@fontsource/roboto": "^5.1.1", "@mui/icons-material": "^6.3.1", "@mui/material": "^6.3.1", - "@notificationapi/core": "^1.2.9", + "@notificationapi/core": "^2.0.0", "javascript-time-ago": "^2.5.10", "liquidjs": "^10.27.2", "rc-virtual-list": "^3.11.5", @@ -1323,9 +1323,9 @@ } }, "node_modules/@notificationapi/core": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@notificationapi/core/-/core-1.2.9.tgz", - "integrity": "sha512-WVpSQsPPhSAfoenz0rv06TFaCVOHn0GVg4xsZFI6GRFpMsctF+vVizOrycR2zgzszLURLA1vwOOdd8f5za21mg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@notificationapi/core/-/core-2.0.0.tgz", + "integrity": "sha512-VkMwkfsQsJWAiqyvYN/WI6s7VcrfNuYKJtl4Zv/uEAXAnvvuKKjxG/AEhxGeL3gctKlt+6ie4WV0XMgfS2L0aw==" }, "node_modules/@oxc-project/types": { "version": "0.133.0", diff --git a/package.json b/package.json index e0962ed..5da0b0b 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@fontsource/roboto": "^5.1.1", "@mui/icons-material": "^6.3.1", "@mui/material": "^6.3.1", - "@notificationapi/core": "^1.2.9", + "@notificationapi/core": "^2.0.0", "javascript-time-ago": "^2.5.10", "liquidjs": "^10.27.2", "rc-virtual-list": "^3.11.5", From e45a0c46496abec01ae9ca4df00c6e1708037b49 Mon Sep 17 00:00:00 2001 From: millerm30 Date: Thu, 24 Sep 2026 08:18:32 -0230 Subject: [PATCH 4/6] Update API URLs in Provider and LiveComponents to use pingram.io --- lib/components/Provider/index.tsx | 4 ++-- src/LiveComponents.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/components/Provider/index.tsx b/lib/components/Provider/index.tsx index 6fc9cd5..69c346b 100644 --- a/lib/components/Provider/index.tsx +++ b/lib/components/Provider/index.tsx @@ -70,8 +70,8 @@ export const NotificationAPIProvider: React.FunctionComponent< }); const defaultConfigs = { - apiURL: 'api.notificationapi.com', - wsURL: 'ws.notificationapi.com', + apiURL: 'api.pingram.io', + wsURL: 'ws.pingram.io', initialLoadMaxCount: 1000, initialLoadMaxAge: new Date(new Date().setMonth(new Date().getMonth() - 3)), playSoundOnNewNotification: false, diff --git a/src/LiveComponents.tsx b/src/LiveComponents.tsx index c5b602b..f137d76 100644 --- a/src/LiveComponents.tsx +++ b/src/LiveComponents.tsx @@ -56,7 +56,7 @@ const LiveComponents: React.FC = ({ }) => { const [clientId, setClientId] = useState('24nojpnrsdc53fkslha0roov05'); const [userId, setUserId] = useState('sahand'); - const [apiUrl, setApiUrl] = useState('api.notificationapi.com'); + const [apiUrl, setApiUrl] = useState('api.pingram.io'); const [debugMode, setDebugMode] = useState(true); const [error] = useState(null); const [preferencesPopupVisibility, setPreferencesPopupVisiblity] = From e3fc283b422c51f778c6ba0866a1d73c62154049 Mon Sep 17 00:00:00 2001 From: millerm30 Date: Thu, 24 Sep 2026 08:21:26 -0230 Subject: [PATCH 5/6] damn cooldown check --- .github/package-cooldown-allowlist.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/package-cooldown-allowlist.json b/.github/package-cooldown-allowlist.json index f924f99..e9ba6e7 100644 --- a/.github/package-cooldown-allowlist.json +++ b/.github/package-cooldown-allowlist.json @@ -4,10 +4,10 @@ "allowlist": [ { "name": "@notificationapi/core", - "version": "1.2.9", + "version": "2.0.0", "reason": "Required for NotificationAPI React Client SDK, internal dependency", "approvedBy": "millerm30", - "approvedAt": "2026-09-08" + "approvedAt": "2026-09-24" } ] } From 95685ca64e8fbcf03357c51e10b85d46e618c6be Mon Sep 17 00:00:00 2001 From: millerm30 Date: Thu, 24 Sep 2026 08:22:13 -0230 Subject: [PATCH 6/6] 2.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 782adf7..3c724c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notificationapi/react", - "version": "1.10.21", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notificationapi/react", - "version": "1.10.21", + "version": "2.0.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", diff --git a/package.json b/package.json index 5da0b0b..48d5718 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@notificationapi/react", "private": false, - "version": "1.10.21", + "version": "2.0.0", "type": "module", "overrides": { "esbuild": "^0.25.0",