@checkstack/notification-frontend 0.0.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,175 @@
1
+ # @checkstack/notification-frontend
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/auth-frontend@0.0.2
10
+ - @checkstack/common@0.0.2
11
+ - @checkstack/frontend-api@0.0.2
12
+ - @checkstack/notification-common@0.0.2
13
+ - @checkstack/signal-frontend@0.0.2
14
+ - @checkstack/ui@0.0.2
15
+
16
+ ## 0.1.4
17
+
18
+ ### Patch Changes
19
+
20
+ - a65e002: Add compile-time type safety for Lucide icon names
21
+
22
+ - Add `LucideIconName` type and `lucideIconSchema` Zod schema to `@checkstack/common`
23
+ - Update backend interfaces (`AuthStrategy`, `NotificationStrategy`, `IntegrationProvider`, `CommandDefinition`) to use `LucideIconName`
24
+ - Update RPC contracts to use `lucideIconSchema` for proper type inference across RPC boundaries
25
+ - Simplify `SocialProviderButton` to use `DynamicIcon` directly (removes 30+ lines of pascalCase conversion)
26
+ - Replace static `iconMap` in `SearchDialog` with `DynamicIcon` for dynamic icon rendering
27
+ - Add fallback handling in `DynamicIcon` when icon name isn't found
28
+ - Fix legacy kebab-case icon names to PascalCase: `mail`→`Mail`, `send`→`Send`, `github`→`Github`, `key-round`→`KeyRound`, `network`→`Network`, `AlertCircle`→`CircleAlert`
29
+
30
+ - ae33df2: Move command palette from dashboard to centered navbar position
31
+
32
+ - Converted `command-frontend` into a plugin with `NavbarCenterSlot` extension
33
+ - Added compact `NavbarSearch` component with responsive search trigger
34
+ - Moved `SearchDialog` from dashboard-frontend to command-frontend
35
+ - Keyboard shortcut (⌘K / Ctrl+K) now works on every page
36
+ - Renamed navbar slots for clarity:
37
+ - `NavbarSlot` → `NavbarRightSlot`
38
+ - `NavbarMainSlot` → `NavbarLeftSlot`
39
+ - Added new `NavbarCenterSlot` for centered content
40
+
41
+ - 32ea706: ### User Menu Loading State Fix
42
+
43
+ Fixed user menu items "popping in" one after another due to independent async permission checks.
44
+
45
+ **Changes:**
46
+
47
+ - Added `UserMenuItemsContext` interface with `permissions` and `hasCredentialAccount` to `@checkstack/frontend-api`
48
+ - `LoginNavbarAction` now pre-fetches all permissions and credential account info before rendering the menu
49
+ - All user menu item components now use the passed context for synchronous permission checks instead of async hooks
50
+ - Uses `qualifyPermissionId` helper for fully-qualified permission IDs
51
+
52
+ **Result:** All menu items appear simultaneously when the user menu opens.
53
+
54
+ - Updated dependencies [52231ef]
55
+ - Updated dependencies [b0124ef]
56
+ - Updated dependencies [54cc787]
57
+ - Updated dependencies [a65e002]
58
+ - Updated dependencies [ae33df2]
59
+ - Updated dependencies [a65e002]
60
+ - Updated dependencies [32ea706]
61
+ - @checkstack/auth-frontend@0.3.0
62
+ - @checkstack/ui@0.1.2
63
+ - @checkstack/common@0.2.0
64
+ - @checkstack/frontend-api@0.1.0
65
+ - @checkstack/notification-common@0.1.1
66
+ - @checkstack/signal-frontend@0.1.1
67
+
68
+ ## 0.1.3
69
+
70
+ ### Patch Changes
71
+
72
+ - Updated dependencies [1bf71bb]
73
+ - @checkstack/auth-frontend@0.2.1
74
+
75
+ ## 0.1.2
76
+
77
+ ### Patch Changes
78
+
79
+ - Updated dependencies [e26c08e]
80
+ - @checkstack/auth-frontend@0.2.0
81
+
82
+ ## 0.1.1
83
+
84
+ ### Patch Changes
85
+
86
+ - Updated dependencies [0f8cc7d]
87
+ - @checkstack/frontend-api@0.0.3
88
+ - @checkstack/auth-frontend@0.1.1
89
+ - @checkstack/ui@0.1.1
90
+
91
+ ## 0.1.0
92
+
93
+ ### Minor Changes
94
+
95
+ - b55fae6: Added realtime Signal Service for backend-to-frontend push notifications via WebSockets.
96
+
97
+ ## New Packages
98
+
99
+ - **@checkstack/signal-common**: Shared types including `Signal`, `SignalService`, `createSignal()`, and WebSocket protocol messages
100
+ - **@checkstack/signal-backend**: `SignalServiceImpl` with EventBus integration and Bun WebSocket handler using native pub/sub
101
+ - **@checkstack/signal-frontend**: React `SignalProvider` and `useSignal()` hook for consuming typed signals
102
+
103
+ ## Changes
104
+
105
+ - **@checkstack/backend-api**: Added `coreServices.signalService` reference for plugins to emit signals
106
+ - **@checkstack/backend**: Integrated WebSocket server at `/api/signals/ws` with session-based authentication
107
+
108
+ ## Usage
109
+
110
+ Backend plugins can emit signals:
111
+
112
+ ```typescript
113
+ import { coreServices } from "@checkstack/backend-api";
114
+ import { NOTIFICATION_RECEIVED } from "@checkstack/notification-common";
115
+
116
+ const signalService = context.signalService;
117
+ await signalService.sendToUser(NOTIFICATION_RECEIVED, userId, { ... });
118
+ ```
119
+
120
+ Frontend components subscribe to signals:
121
+
122
+ ```tsx
123
+ import { useSignal } from "@checkstack/signal-frontend";
124
+ import { NOTIFICATION_RECEIVED } from "@checkstack/notification-common";
125
+
126
+ useSignal(NOTIFICATION_RECEIVED, (payload) => {
127
+ // Handle realtime notification
128
+ });
129
+ ```
130
+
131
+ - b354ab3: # Strategy Instructions Support & Telegram Notification Plugin
132
+
133
+ ## Strategy Instructions Interface
134
+
135
+ Added `adminInstructions` and `userInstructions` optional fields to the `NotificationStrategy` interface. These allow strategies to export markdown-formatted setup guides that are displayed in the configuration UI:
136
+
137
+ - **`adminInstructions`**: Shown when admins configure platform-wide strategy settings (e.g., how to create API keys)
138
+ - **`userInstructions`**: Shown when users configure their personal settings (e.g., how to link their account)
139
+
140
+ ### Updated Components
141
+
142
+ - `StrategyConfigCard` now accepts an `instructions` prop and renders it before config sections
143
+ - `StrategyCard` passes `adminInstructions` to `StrategyConfigCard`
144
+ - `UserChannelCard` renders `userInstructions` when users need to connect
145
+
146
+ ## New Telegram Notification Plugin
147
+
148
+ Added `@checkstack/notification-telegram-backend` plugin for sending notifications via Telegram:
149
+
150
+ - Uses [grammY](https://grammy.dev/) framework for Telegram Bot API integration
151
+ - Sends messages with MarkdownV2 formatting and inline keyboard buttons for actions
152
+ - Includes comprehensive admin instructions for bot setup via @BotFather
153
+ - Includes user instructions for account linking
154
+
155
+ ### Configuration
156
+
157
+ Admins need to configure a Telegram Bot Token obtained from @BotFather.
158
+
159
+ ### User Linking
160
+
161
+ The strategy uses `contactResolution: { type: "custom" }` for Telegram Login Widget integration. Full frontend integration for the Login Widget is pending future work.
162
+
163
+ ### Patch Changes
164
+
165
+ - Updated dependencies [eff5b4e]
166
+ - Updated dependencies [ffc28f6]
167
+ - Updated dependencies [32f2535]
168
+ - Updated dependencies [b55fae6]
169
+ - Updated dependencies [b354ab3]
170
+ - @checkstack/ui@0.1.0
171
+ - @checkstack/common@0.1.0
172
+ - @checkstack/notification-common@0.1.0
173
+ - @checkstack/auth-frontend@0.1.0
174
+ - @checkstack/signal-frontend@0.1.0
175
+ - @checkstack/frontend-api@0.0.2
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@checkstack/notification-frontend",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "src/index.tsx",
6
+ "checkstack": {
7
+ "type": "frontend"
8
+ },
9
+ "scripts": {
10
+ "typecheck": "tsc --noEmit",
11
+ "lint": "bun run lint:code",
12
+ "lint:code": "eslint . --max-warnings 0"
13
+ },
14
+ "dependencies": {
15
+ "@checkstack/notification-common": "workspace:*",
16
+ "@checkstack/frontend-api": "workspace:*",
17
+ "@checkstack/auth-frontend": "workspace:*",
18
+ "@checkstack/signal-frontend": "workspace:*",
19
+ "@checkstack/common": "workspace:*",
20
+ "@checkstack/ui": "workspace:*",
21
+ "react": "^18.2.0",
22
+ "react-router-dom": "^6.22.0",
23
+ "lucide-react": "^0.344.0"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "^5.0.0",
27
+ "@types/react": "^18.2.0",
28
+ "@checkstack/tsconfig": "workspace:*",
29
+ "@checkstack/scripts": "workspace:*"
30
+ }
31
+ }
@@ -0,0 +1,283 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { Bell, CheckCheck } from "lucide-react";
4
+ import {
5
+ Badge,
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ DropdownMenuSeparator,
11
+ Button,
12
+ stripMarkdown,
13
+ } from "@checkstack/ui";
14
+ import { useApi, rpcApiRef } from "@checkstack/frontend-api";
15
+ import { useSignal } from "@checkstack/signal-frontend";
16
+ import { resolveRoute } from "@checkstack/common";
17
+ import type { Notification } from "@checkstack/notification-common";
18
+ import {
19
+ NotificationApi,
20
+ NOTIFICATION_RECEIVED,
21
+ NOTIFICATION_COUNT_CHANGED,
22
+ NOTIFICATION_READ,
23
+ notificationRoutes,
24
+ } from "@checkstack/notification-common";
25
+ import { authApiRef } from "@checkstack/auth-frontend/api";
26
+
27
+ export const NotificationBell = () => {
28
+ const authApi = useApi(authApiRef);
29
+ const { data: session, isPending: isAuthLoading } = authApi.useSession();
30
+ const rpcApi = useApi(rpcApiRef);
31
+ const notificationClient = rpcApi.forPlugin(NotificationApi);
32
+
33
+ const [unreadCount, setUnreadCount] = useState(0);
34
+ const [recentNotifications, setRecentNotifications] = useState<
35
+ Notification[]
36
+ >([]);
37
+ const [isOpen, setIsOpen] = useState(false);
38
+ const [loading, setLoading] = useState(true);
39
+
40
+ const fetchUnreadCount = useCallback(async () => {
41
+ // Skip fetch if not authenticated
42
+ if (!session) return;
43
+ try {
44
+ const { count } = await notificationClient.getUnreadCount();
45
+ setUnreadCount(count);
46
+ } catch (error) {
47
+ console.error("Failed to fetch unread count:", error);
48
+ }
49
+ }, [notificationClient, session]);
50
+
51
+ const fetchRecentNotifications = useCallback(async () => {
52
+ // Skip fetch if not authenticated
53
+ if (!session) return;
54
+ try {
55
+ const { notifications } = await notificationClient.getNotifications({
56
+ limit: 5,
57
+ offset: 0,
58
+ unreadOnly: true, // Only show unread notifications in the dropdown
59
+ });
60
+ setRecentNotifications(notifications);
61
+ } catch (error) {
62
+ console.error("Failed to fetch notifications:", error);
63
+ }
64
+ }, [notificationClient, session]);
65
+
66
+ // Initial fetch
67
+ useEffect(() => {
68
+ if (!session) {
69
+ setLoading(false);
70
+ return;
71
+ }
72
+ const init = async () => {
73
+ await Promise.all([fetchUnreadCount(), fetchRecentNotifications()]);
74
+ setLoading(false);
75
+ };
76
+ void init();
77
+ }, [fetchUnreadCount, fetchRecentNotifications, session]);
78
+
79
+ // ==========================================================================
80
+ // REALTIME SIGNAL SUBSCRIPTIONS (replaces polling)
81
+ // ==========================================================================
82
+
83
+ // Handle new notification received
84
+ useSignal(
85
+ NOTIFICATION_RECEIVED,
86
+ useCallback((payload) => {
87
+ // Increment unread count
88
+ setUnreadCount((prev) => prev + 1);
89
+
90
+ // Add to recent notifications if dropdown is open
91
+ setRecentNotifications((prev) => [
92
+ {
93
+ id: payload.id,
94
+ title: payload.title,
95
+ body: payload.body,
96
+ importance: payload.importance,
97
+ userId: "", // Not needed for display
98
+ isRead: false,
99
+ createdAt: new Date(),
100
+ },
101
+ ...prev.slice(0, 4), // Keep only 5 items
102
+ ]);
103
+ }, [])
104
+ );
105
+
106
+ // Handle count changes from other sources
107
+ useSignal(
108
+ NOTIFICATION_COUNT_CHANGED,
109
+ useCallback((payload) => {
110
+ setUnreadCount(payload.unreadCount);
111
+ }, [])
112
+ );
113
+
114
+ // Handle notification marked as read
115
+ useSignal(
116
+ NOTIFICATION_READ,
117
+ useCallback((payload) => {
118
+ if (payload.notificationId) {
119
+ // Single notification marked as read - remove from list
120
+ setRecentNotifications((prev) =>
121
+ prev.filter((n) => n.id !== payload.notificationId)
122
+ );
123
+ setUnreadCount((prev) => Math.max(0, prev - 1));
124
+ } else {
125
+ // All marked as read - clear the list
126
+ setRecentNotifications([]);
127
+ setUnreadCount(0);
128
+ }
129
+ }, [])
130
+ );
131
+
132
+ // ==========================================================================
133
+
134
+ // Fetch notifications when dropdown opens
135
+ useEffect(() => {
136
+ if (isOpen) {
137
+ void fetchRecentNotifications();
138
+ }
139
+ }, [isOpen, fetchRecentNotifications]);
140
+
141
+ const handleMarkAllAsRead = async () => {
142
+ try {
143
+ await notificationClient.markAsRead({});
144
+ setUnreadCount(0);
145
+ setRecentNotifications([]);
146
+ } catch (error) {
147
+ console.error("Failed to mark all as read:", error);
148
+ }
149
+ };
150
+
151
+ const handleClose = useCallback(() => {
152
+ setIsOpen(false);
153
+ }, []);
154
+
155
+ // Hide notification bell for unauthenticated users
156
+ if (isAuthLoading || !session) {
157
+ return;
158
+ }
159
+
160
+ if (loading) {
161
+ return (
162
+ <Button variant="ghost" size="icon" className="relative" disabled>
163
+ <Bell className="h-5 w-5" />
164
+ </Button>
165
+ );
166
+ }
167
+
168
+ return (
169
+ <DropdownMenu>
170
+ <DropdownMenuTrigger
171
+ onClick={() => {
172
+ setIsOpen(!isOpen);
173
+ }}
174
+ >
175
+ <Button variant="ghost" size="icon" className="relative group">
176
+ <Bell className="h-5 w-5 transition-transform group-hover:scale-110" />
177
+ {unreadCount > 0 && (
178
+ <span className="absolute -top-1 -right-1 flex h-5 min-w-[20px] items-center justify-center">
179
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-destructive opacity-75" />
180
+ <Badge
181
+ variant="destructive"
182
+ className="relative h-5 min-w-[20px] flex items-center justify-center p-0 text-xs font-bold"
183
+ >
184
+ {unreadCount > 99 ? "99+" : unreadCount}
185
+ </Badge>
186
+ </span>
187
+ )}
188
+ </Button>
189
+ </DropdownMenuTrigger>
190
+ <DropdownMenuContent
191
+ isOpen={isOpen}
192
+ onClose={handleClose}
193
+ className="w-80"
194
+ >
195
+ {/* Header */}
196
+ <div className="flex items-center justify-between px-3 py-2 border-b border-border">
197
+ <span className="font-semibold text-sm">Notifications</span>
198
+ {unreadCount > 0 && (
199
+ <Button
200
+ variant="ghost"
201
+ size="sm"
202
+ className="h-6 text-xs"
203
+ onClick={() => {
204
+ void handleMarkAllAsRead();
205
+ }}
206
+ >
207
+ <CheckCheck className="h-3 w-3 mr-1" />
208
+ Mark all read
209
+ </Button>
210
+ )}
211
+ </div>
212
+
213
+ {/* Notification List */}
214
+ <div className="max-h-[400px] overflow-y-auto">
215
+ {recentNotifications.length === 0 ? (
216
+ <div className="px-3 py-6 text-center text-sm text-muted-foreground">
217
+ No unread notifications
218
+ </div>
219
+ ) : (
220
+ <>
221
+ {recentNotifications.map((notification) => (
222
+ <DropdownMenuItem
223
+ key={notification.id}
224
+ className={`flex flex-col items-start gap-1 px-3 py-2 cursor-pointer ${
225
+ notification.importance === "critical"
226
+ ? "border-l-2 border-l-destructive"
227
+ : notification.importance === "warning"
228
+ ? "border-l-2 border-l-warning"
229
+ : ""
230
+ }`}
231
+ >
232
+ <div
233
+ className={`font-medium text-sm ${
234
+ notification.importance === "critical"
235
+ ? "text-destructive"
236
+ : notification.importance === "warning"
237
+ ? "text-warning"
238
+ : "text-foreground"
239
+ }`}
240
+ >
241
+ {notification.title}
242
+ </div>
243
+ <div className="text-xs text-muted-foreground line-clamp-2">
244
+ {stripMarkdown(notification.body)}
245
+ </div>
246
+ {notification.action && (
247
+ <div className="flex gap-2 mt-1">
248
+ <Link
249
+ to={notification.action.url}
250
+ className="text-xs text-primary hover:underline"
251
+ onClick={(e: React.MouseEvent) => {
252
+ e.stopPropagation();
253
+ }}
254
+ >
255
+ {notification.action.label}
256
+ </Link>
257
+ </div>
258
+ )}
259
+ </DropdownMenuItem>
260
+ ))}
261
+ </>
262
+ )}
263
+ </div>
264
+
265
+ <DropdownMenuSeparator />
266
+
267
+ {/* Footer */}
268
+ <DropdownMenuItem
269
+ onClick={() => {
270
+ handleClose();
271
+ }}
272
+ >
273
+ <Link
274
+ to={resolveRoute(notificationRoutes.routes.home)}
275
+ className="w-full text-center text-sm text-primary"
276
+ >
277
+ View all notifications
278
+ </Link>
279
+ </DropdownMenuItem>
280
+ </DropdownMenuContent>
281
+ </DropdownMenu>
282
+ );
283
+ };
@@ -0,0 +1,163 @@
1
+ import {
2
+ StrategyConfigCard,
3
+ type ConfigSection,
4
+ type LucideIconName,
5
+ } from "@checkstack/ui";
6
+
7
+ /**
8
+ * Strategy data from getDeliveryStrategies endpoint
9
+ */
10
+ export interface DeliveryStrategy {
11
+ qualifiedId: string;
12
+ displayName: string;
13
+ description?: string;
14
+ icon?: LucideIconName;
15
+ ownerPluginId: string;
16
+ contactResolution: {
17
+ type:
18
+ | "auth-email"
19
+ | "auth-provider"
20
+ | "user-config"
21
+ | "oauth-link"
22
+ | "custom";
23
+ provider?: string;
24
+ field?: string;
25
+ };
26
+ requiresUserConfig: boolean;
27
+ requiresOAuthLink: boolean;
28
+ configSchema: Record<string, unknown>;
29
+ userConfigSchema?: Record<string, unknown>;
30
+ /** Layout config schema for admin customization (logo, colors, etc.) */
31
+ layoutConfigSchema?: Record<string, unknown>;
32
+ enabled: boolean;
33
+ config?: Record<string, unknown>;
34
+ /** Current layout config values */
35
+ layoutConfig?: Record<string, unknown>;
36
+ /** Markdown instructions for admins (setup guides, etc.) */
37
+ adminInstructions?: string;
38
+ }
39
+
40
+ export interface StrategyCardProps {
41
+ strategy: DeliveryStrategy;
42
+ onUpdate: (
43
+ strategyId: string,
44
+ enabled: boolean,
45
+ config?: Record<string, unknown>,
46
+ layoutConfig?: Record<string, unknown>
47
+ ) => Promise<void>;
48
+ saving?: boolean;
49
+ }
50
+
51
+ /**
52
+ * Get contact resolution type badge for notification strategies
53
+ */
54
+ function getResolutionBadge(
55
+ type: DeliveryStrategy["contactResolution"]["type"]
56
+ ) {
57
+ switch (type) {
58
+ case "auth-email": {
59
+ return { label: "User Email", variant: "secondary" as const };
60
+ }
61
+ case "auth-provider": {
62
+ return { label: "Auth Provider", variant: "secondary" as const };
63
+ }
64
+ case "user-config": {
65
+ return { label: "User Config Required", variant: "outline" as const };
66
+ }
67
+ case "oauth-link": {
68
+ return { label: "OAuth Link", variant: "default" as const };
69
+ }
70
+ default: {
71
+ return { label: "Custom", variant: "outline" as const };
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Admin card for configuring a delivery strategy.
78
+ * Uses the shared StrategyConfigCard component.
79
+ */
80
+ export function StrategyCard({
81
+ strategy,
82
+ onUpdate,
83
+ saving,
84
+ }: StrategyCardProps) {
85
+ // Build badges array from strategy properties
86
+ const badges = [
87
+ getResolutionBadge(strategy.contactResolution.type),
88
+ ...(strategy.requiresOAuthLink
89
+ ? [{ label: "OAuth", variant: "outline" as const, className: "text-xs" }]
90
+ : []),
91
+ ];
92
+
93
+ // Check if config is missing - has schema properties but no saved config
94
+ const hasConfigSchema =
95
+ strategy.configSchema &&
96
+ "properties" in strategy.configSchema &&
97
+ Object.keys(strategy.configSchema.properties as Record<string, unknown>)
98
+ .length > 0;
99
+ const configMissing = hasConfigSchema && strategy.config === undefined;
100
+
101
+ const handleToggle = async (id: string, enabled: boolean) => {
102
+ await onUpdate(id, enabled, strategy.config, strategy.layoutConfig);
103
+ };
104
+
105
+ // Build config sections array
106
+ const configSections: ConfigSection[] = [];
107
+
108
+ // Main configuration section
109
+ if (hasConfigSchema) {
110
+ configSections.push({
111
+ id: "config",
112
+ title: "Configuration",
113
+ schema: strategy.configSchema,
114
+ value: strategy.config,
115
+ onSave: async (config) => {
116
+ await onUpdate(
117
+ strategy.qualifiedId,
118
+ strategy.enabled,
119
+ config,
120
+ strategy.layoutConfig
121
+ );
122
+ },
123
+ });
124
+ }
125
+
126
+ // Layout configuration section (if strategy supports it)
127
+ if (strategy.layoutConfigSchema) {
128
+ configSections.push({
129
+ id: "layout",
130
+ title: "Email Layout",
131
+ schema: strategy.layoutConfigSchema,
132
+ value: strategy.layoutConfig,
133
+ onSave: async (layoutConfig) => {
134
+ await onUpdate(
135
+ strategy.qualifiedId,
136
+ strategy.enabled,
137
+ strategy.config,
138
+ layoutConfig
139
+ );
140
+ },
141
+ });
142
+ }
143
+
144
+ return (
145
+ <StrategyConfigCard
146
+ strategy={{
147
+ id: strategy.qualifiedId,
148
+ displayName: strategy.displayName,
149
+ description: strategy.description,
150
+ icon: strategy.icon,
151
+ enabled: strategy.enabled,
152
+ }}
153
+ configSections={configSections}
154
+ onToggle={handleToggle}
155
+ saving={saving}
156
+ badges={badges}
157
+ subtitle={`From: ${strategy.ownerPluginId}`}
158
+ disabledWarning="Enable this channel to allow users to receive notifications"
159
+ configMissing={configMissing}
160
+ instructions={strategy.adminInstructions}
161
+ />
162
+ );
163
+ }