Build a new channel Connect button (e.g. WhatsApp, Discord, LINE) in @novu/js and @novu/react following the existing SlackConnectButton, MsTeamsConnectButton, and TelegramConnectButton pattern. Use when adding connect/disconnect UI for a new chat channel/provider to the SDK — covering the SolidJS core component, the React wrapper, the channelConnections (OAuth) vs channelEndpoints (deep-link) data layer, the component registry, and package exports.
76
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Build <Channel>ConnectButton for a new chat provider, mirroring Slack, MsTeams,
and Telegram. The button lets a subscriber connect (open OAuth popup / deep link)
and disconnect a channel, with loading + connected states.
packages/js (SolidJS core) packages/react (public wrapper)
───────────────────────── ──────────────────────────────
<Channel>ConnectButton.tsx ◄──── Default<Channel>ConnectButton.tsx (Mounter → mountComponent)
uses a data hook <Channel>ConnectButton.tsx (memo → NovuUI → withRenderer)
calls novu.channel* SDK
registered in Renderer.tsxThe React component mounts the SolidJS component by name through novuUI.mountComponent.
That name must be registered in packages/js/src/ui/components/Renderer.tsx.
This single decision drives which SDK module, hook, and props you use.
| Connection-based — like Slack, MS Teams | Endpoint-based — like Telegram | |
|---|---|---|
| Use when | Provider authorizes at workspace/tenant level via OAuth | Subscriber links by opening a deep link; no workspace auth |
| SDK module | novu.channelConnections | novu.channelEndpoints |
| Connect call | generateConnectOAuthUrl() → OAuth popup URL | link({ integrationIdentifier }) → deep-link URL |
| Detect / poll | get(connectionIdentifier) | list({ providerId, integrationIdentifier, limit: 1 }) |
| Data hook | reuse useChannelConnection | new use<Channel>Connection (list-based, copy useTelegramConnection) |
| Extra props | connectionIdentifier, context, scope, connectionMode, autoLinkUser | none beyond the base props |
| Identifier | buildDefaultConnectionIdentifier(...) from components/constants.ts | not needed |
If unsure: OAuth/app-install provider → connection-based; bot deep-link/token provider → endpoint-based.
Copy the nearest sibling for your model (Slack/MS Teams = connection, Telegram = endpoint), then rename. Full templates: see reference.md.
Core — packages/js
src/ui/icons/<Channel>Colored.tsx — brand icon (copy TelegramColored.tsx)src/ui/components/<channel>-connect-button/<Channel>ConnectButton.tsxsrc/ui/api/hooks/use<Channel>Connection.tssrc/ui/components/Renderer.tsx — import it, add to novuComponents, add the name string to CHANNEL_COMPONENTSsrc/ui/components/index.ts — export * from './<channel>-connect-button/<Channel>ConnectButton'src/ui/index.ts — re-export <Channel>ConnectButtonProps in the type blockReact — packages/react
src/components/<channel>-connect-button/Default<Channel>ConnectButton.tsxsrc/components/<channel>-connect-button/<Channel>ConnectButton.tsxsrc/components/index.ts — export * from './<channel>-connect-button/<Channel>ConnectButton'src/index.ts — add the component to the value export block and <Channel>ConnectButtonProps to the type blockUsually NOT needed
channelConnections / channelEndpoints SDK modules already cover both models. Only add a new method under packages/js/src/channel-* if the provider needs a brand-new server call (rare). Backend lives in apps/api/src/app/channel-connections/.libs/internal-sdk (auto-generated).Every connect button implements the same state machine — keep it identical:
<Show when={!loading()} fallback={<Loader/>}>; derive isConnected() from the hook's connection/endpoint, and isLoading() = loading() || actionLoading().disconnect(identifier) → onDisconnectSuccess() / onDisconnectError(err).setActionLoading(true), get the URL, window.open(url, '_blank', 'noopener,noreferrer'), then start polling.2500ms, 120_000ms timeout) or backoff (see MS Teams). Use a one-shot committed flag / ref so only the first success-or-timeout fires side effects. Clear the timer in onCleanup.mutate(found) + onConnectSuccess(identifier); timeout: onConnectError(new Error(...)).channelConnectButtonContainer | Button | Inner | Icon | Label, each passed { connected } context. Icons go through IconRendererWrapper (keys channelConnect / channelConnected) with a fallback to <Channel>Colored / CheckCircleFill. Do not add new appearance keys — they are shared across all connect buttons.integrationIdentifier (required), subscriberId?, onConnectSuccess?, onConnectError?, onDisconnectSuccess?, onDisconnectError?, connectLabel?, connectedLabel?.<Show> over ternaries, read props lazily (() => props.x), cleanup via onCleanup. The React layer is just a mounting shim.name in mountComponent({ name: '<Channel>ConnectButton' }) must equal the key in novuComponents and be listed in CHANNEL_COMPONENTS, or it renders through the wrong path.<Channel>ConnectButtonProps is defined in the core and imported by React from @novu/js/ui — do not redefine it.return, no nested ternaries.pnpm build (required after changing packages/).playground/nextjs (copy src/components/telegram-end-user-connect.tsx) that wraps <Channel>ConnectButton in <NovuProvider>, then connect/disconnect against a real integration.67de0fa
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.