diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9de42f047..4db41fc96 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,6 +63,8 @@ jobs: echo "LIVEBLOCKS_SECRET=sk_test" >> apps/app/.env.local echo "BASEHUB_TOKEN=${{ secrets.BASEHUB_TOKEN }}" >> apps/app/.env.local echo "VERCEL_PROJECT_PRODUCTION_URL=http://localhost:3002" >> apps/app/.env.local + echo "KNOCK_API_KEY=test" >> apps/app/.env.local + echo "KNOCK_FEED_CHANNEL_ID=test" >> apps/app/.env.local echo "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_JA==" >> apps/app/.env.local echo "NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in" >> apps/app/.env.local diff --git a/apps/api/.env.example b/apps/api/.env.example index fd29f1e65..71bd48d36 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -14,6 +14,8 @@ SVIX_TOKEN="" LIVEBLOCKS_SECRET="" BASEHUB_TOKEN="" VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3002" +KNOCK_API_KEY="" +KNOCK_FEED_CHANNEL_ID="" # Client NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" diff --git a/apps/app/.env.example b/apps/app/.env.example index c62797519..053ee5b31 100644 --- a/apps/app/.env.example +++ b/apps/app/.env.example @@ -14,6 +14,9 @@ SVIX_TOKEN="" LIVEBLOCKS_SECRET="" BASEHUB_TOKEN="" VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000" +KNOCK_API_KEY="" +KNOCK_FEED_CHANNEL_ID="" +KNOCK_SECRET_API_KEY="" # Client NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" @@ -26,4 +29,4 @@ NEXT_PUBLIC_POSTHOG_KEY="" NEXT_PUBLIC_POSTHOG_HOST="" NEXT_PUBLIC_APP_URL="http://localhost:3000" NEXT_PUBLIC_WEB_URL="http://localhost:3001" -NEXT_PUBLIC_DOCS_URL="http://localhost:3004" \ No newline at end of file +NEXT_PUBLIC_DOCS_URL="http://localhost:3004" diff --git a/apps/app/app/(authenticated)/components/sidebar.tsx b/apps/app/app/(authenticated)/components/sidebar.tsx index 05fab4801..f7080d87f 100644 --- a/apps/app/app/(authenticated)/components/sidebar.tsx +++ b/apps/app/app/(authenticated)/components/sidebar.tsx @@ -2,6 +2,7 @@ import { OrganizationSwitcher, UserButton } from '@repo/auth/client'; import { ModeToggle } from '@repo/design-system/components/mode-toggle'; +import { Button } from '@repo/design-system/components/ui/button'; import { Collapsible, CollapsibleContent, @@ -33,8 +34,10 @@ import { useSidebar, } from '@repo/design-system/components/ui/sidebar'; import { cn } from '@repo/design-system/lib/utils'; +import { NotificationsTrigger } from '@repo/notifications/components/trigger'; import { AnchorIcon, + BellIcon, BookOpenIcon, BotIcon, ChevronRightIcon, @@ -331,7 +334,14 @@ export const GlobalSidebar = ({ children }: GlobalSidebarProperties) => { }, }} /> - +
+ + + + +
diff --git a/apps/app/app/(authenticated)/layout.tsx b/apps/app/app/(authenticated)/layout.tsx index ea8bc8021..fe8de19bb 100644 --- a/apps/app/app/(authenticated)/layout.tsx +++ b/apps/app/app/(authenticated)/layout.tsx @@ -2,6 +2,7 @@ import { env } from '@/env'; import { auth, currentUser } from '@repo/auth/server'; import { SidebarProvider } from '@repo/design-system/components/ui/sidebar'; import { showBetaFeature } from '@repo/feature-flags'; +import { NotificationsProvider } from '@repo/notifications/components/provider'; import { secure } from '@repo/security'; import type { ReactNode } from 'react'; import { PostHogIdentifier } from './components/posthog-identifier'; @@ -21,21 +22,23 @@ const AppLayout = async ({ children }: AppLayoutProperties) => { const betaFeature = await showBetaFeature(); if (!user) { - redirectToSignIn(); + return redirectToSignIn(); } return ( - - - {betaFeature && ( -
- Beta feature now available -
- )} - {children} -
- -
+ + + + {betaFeature && ( +
+ Beta feature now available +
+ )} + {children} +
+ +
+
); }; diff --git a/apps/app/env.ts b/apps/app/env.ts index 1d9671108..f234cf22f 100644 --- a/apps/app/env.ts +++ b/apps/app/env.ts @@ -5,6 +5,7 @@ import { keys as database } from '@repo/database/keys'; import { keys as email } from '@repo/email/keys'; import { keys as flags } from '@repo/feature-flags/keys'; import { keys as core } from '@repo/next-config/keys'; +import { keys as notifications } from '@repo/notifications/keys'; import { keys as observability } from '@repo/observability/keys'; import { keys as security } from '@repo/security/keys'; import { keys as webhooks } from '@repo/webhooks/keys'; @@ -19,6 +20,7 @@ export const env = createEnv({ database(), email(), flags(), + notifications(), observability(), security(), webhooks(), diff --git a/apps/app/package.json b/apps/app/package.json index d643c6411..73836940e 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -19,6 +19,7 @@ "@repo/design-system": "workspace:*", "@repo/feature-flags": "workspace:*", "@repo/next-config": "workspace:*", + "@repo/notifications": "workspace:*", "@repo/observability": "workspace:*", "@repo/security": "workspace:*", "@repo/seo": "workspace:*", diff --git a/apps/web/.env.example b/apps/web/.env.example index 7bf9dd822..8a99e1420 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -13,7 +13,9 @@ ARCJET_KEY="" SVIX_TOKEN="" LIVEBLOCKS_SECRET="" BASEHUB_TOKEN="" -# VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3001" +VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3001" +KNOCK_API_KEY="" +KNOCK_FEED_CHANNEL_ID="" # Client NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" diff --git a/docs/images/authors/knock/j_everhart383.jpg b/docs/images/authors/knock/j_everhart383.jpg new file mode 100644 index 000000000..85fb75339 Binary files /dev/null and b/docs/images/authors/knock/j_everhart383.jpg differ diff --git a/docs/images/authors/knock/logo.jpg b/docs/images/authors/knock/logo.jpg new file mode 100644 index 000000000..e05e36fc0 Binary files /dev/null and b/docs/images/authors/knock/logo.jpg differ diff --git a/docs/mint.json b/docs/mint.json index 8b02f4b42..6f6e1a6cd 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -127,6 +127,7 @@ "packages/next-config/bundle-analysis" ] }, + "packages/notifications", "packages/payments", { "group": "Security", diff --git a/docs/packages/notifications.mdx b/docs/packages/notifications.mdx new file mode 100644 index 000000000..9b0fe267f --- /dev/null +++ b/docs/packages/notifications.mdx @@ -0,0 +1,72 @@ +--- +title: Notifications +description: In-app notifications for your users. +--- + +import { Authors } from '/snippets/authors.mdx'; + + + +next-forge offers a notifications package that allows you to send in-app notifications to your users. By default, it uses [Knock](https://knock.app/), a cross-channel notification platform that supports in-app, email, SMS, push, and chat notifications. Knock allows you to centralize your notification logic and templates in one place and [orchestrate complex workflows](https://docs.knock.app/designing-workflows/overview) with things like branching, batching, throttling, delays, and conditional sending. + +## Setup + +To use the notifications package, you need to add the required environment variables to your project, as specified in the `packages/notifications/keys.ts` file. + +## In-app notifications feed + +To render an in-app notifications feed, import the `NotificationsTrigger` component from the `@repo/notifications` package and use it in your app. We've already added this to the sidebar in the example app: + +```tsx apps/app/app/(authenticated)/components/sidebar.tsx +import { NotificationsTrigger } from '@repo/notifications/components/trigger'; + + + + +``` + +Pressing the button will open the in-app notifications feed, which displays all of the notifications for the current user. + +## Send a notification + +Knock sends notifications using workflows. To send an in-app notification, create a new [workflow](https://docs.knock.app/concepts/workflows) in the Knock dashboard that uses the [`in-app` channel provider](https://docs.knock.app/integrations/in-app/knock) and create a corresponding message template. + +Then you can [trigger that workflow](https://docs.knock.app/send-notifications/triggering-workflows) for a particular user in your app, passing in the necessary data to populate the message template: + +```tsx notify.ts +import { notifications } from '@repo/notifications'; + +await notifications.workflows.trigger('workflow-key', { + recipients: [{ + id: 'user-id', + email: 'user-email', + }], + data: { + message: 'Hello, world!', + }, +}); +``` + +## Multi-channel notifications + +Using Knock, you can add additional channel providers to your workflow to send notifications via email, SMS, push, or chat. To do this, create a new [channel provider](https://docs.knock.app/integrations) in the Knock dashboard, follow any configuration instructions for that provider, and add it to your workflow as a channel step. diff --git a/packages/design-system/components/mode-toggle.tsx b/packages/design-system/components/mode-toggle.tsx index 784f324e4..59b6b468a 100644 --- a/packages/design-system/components/mode-toggle.tsx +++ b/packages/design-system/components/mode-toggle.tsx @@ -23,7 +23,7 @@ export const ModeToggle = () => {