From ec6f9ad8d7afb245f8ebfaf1bf3aaefc760b61fa Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Thu, 9 Jan 2025 15:09:41 -0500 Subject: [PATCH] fix: allow falsey userIds for auth and gracefully handle (#343) --- .changeset/healthy-ladybugs-drum.md | 6 ++++ examples/nextjs-example/next-env.d.ts | 2 +- examples/nextjs-example/pages/api/identify.ts | 15 +++++--- examples/nextjs-example/pages/api/notify.ts | 4 +-- examples/nextjs-example/pages/index.tsx | 26 ++------------ packages/client/src/clients/feed/feed.ts | 35 +++++++++++++------ packages/client/src/knock.ts | 10 ------ 7 files changed, 46 insertions(+), 52 deletions(-) create mode 100644 .changeset/healthy-ladybugs-drum.md diff --git a/.changeset/healthy-ladybugs-drum.md b/.changeset/healthy-ladybugs-drum.md new file mode 100644 index 00000000..caefca8a --- /dev/null +++ b/.changeset/healthy-ladybugs-drum.md @@ -0,0 +1,6 @@ +--- +"nextjs-example": patch +"@knocklabs/client": patch +--- + +fix: ensure feed can render with empty/missing userId values diff --git a/examples/nextjs-example/next-env.d.ts b/examples/nextjs-example/next-env.d.ts index 4f11a03d..a4a7b3f5 100644 --- a/examples/nextjs-example/next-env.d.ts +++ b/examples/nextjs-example/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/nextjs-example/pages/api/identify.ts b/examples/nextjs-example/pages/api/identify.ts index 928237c7..2f931f73 100644 --- a/examples/nextjs-example/pages/api/identify.ts +++ b/examples/nextjs-example/pages/api/identify.ts @@ -26,14 +26,19 @@ export default async function handler( name: name || faker.person.fullName(), }); - const userToken = await Knock.signUserToken(userId, { - expiresInSeconds: process.env.KNOCK_TOKEN_EXPIRES_IN_SECONDS - ? Number(process.env.KNOCK_TOKEN_EXPIRES_IN_SECONDS) - : 3600, - }); + let userToken = undefined; + + if (process.env.KNOCK_SIGNING_KEY) { + userToken = await Knock.signUserToken(userId, { + expiresInSeconds: process.env.KNOCK_TOKEN_EXPIRES_IN_SECONDS + ? Number(process.env.KNOCK_TOKEN_EXPIRES_IN_SECONDS) + : 3600, + }); + } return res.status(200).json({ error: null, user: knockUser, userToken }); } catch (error) { + console.error(error); return res.status(500).json({ error: (error as Error).message || (error as Error).toString(), user: null, diff --git a/examples/nextjs-example/pages/api/notify.ts b/examples/nextjs-example/pages/api/notify.ts index 1a186965..cec8329b 100644 --- a/examples/nextjs-example/pages/api/notify.ts +++ b/examples/nextjs-example/pages/api/notify.ts @@ -21,7 +21,7 @@ export default async function handler( const { message, showToast, userId, tenant, templateType } = req.body; try { - await knockClient.notify(KNOCK_WORKFLOW, { + const response = await knockClient.workflows.trigger(KNOCK_WORKFLOW, { recipients: [userId], actor: userId, tenant, @@ -32,7 +32,7 @@ export default async function handler( }, }); - return res.status(200).json({ error: null }); + return res.status(200).json({ error: null, response }); } catch (error) { return res.status(500).json({ error: (error as Error).message || (error as Error).toString(), diff --git a/examples/nextjs-example/pages/index.tsx b/examples/nextjs-example/pages/index.tsx index acc292bc..bc666a17 100644 --- a/examples/nextjs-example/pages/index.tsx +++ b/examples/nextjs-example/pages/index.tsx @@ -1,13 +1,4 @@ -import { - Box, - Flex, - Heading, - Icon, - Link, - Select, - Spinner, - Text, -} from "@chakra-ui/react"; +import { Box, Flex, Heading, Icon, Link, Select, Text } from "@chakra-ui/react"; import { KnockFeedProvider, KnockProvider, @@ -32,7 +23,7 @@ const TenantLabels = { }; export default function Home() { - const { userId, isLoading, userToken } = useIdentify(); + const { userId, userToken } = useIdentify(); const [tenant, setTenant] = useState(Tenants.TeamA); const tokenRefreshHandler = useCallback(async () => { @@ -43,19 +34,6 @@ export default function Home() { return json.userToken; }, [userId]); - if (isLoading) { - return ( - - - - ); - } - return ( item.id); + const itemIds: string[] = items.map((item) => item.id); /* In the code here we want to optimistically update counts and items @@ -354,15 +360,17 @@ class Feed { if (shouldOptimisticallyRemoveItems) { // If any of the items are unseen or unread, then capture as we'll want to decrement // the counts for these in the metadata we have - const unseenCount = normalizedItems.filter((i) => !i.seen_at).length; - const unreadCount = normalizedItems.filter((i) => !i.read_at).length; + const unseenCount = items.filter((i) => !i.seen_at).length; + const unreadCount = items.filter((i) => !i.read_at).length; // Build the new metadata const updatedMetadata = { ...state.metadata, - total_count: state.metadata.total_count - normalizedItems.length, - unseen_count: state.metadata.unseen_count - unseenCount, - unread_count: state.metadata.unread_count - unreadCount, + // Ensure that the counts don't ever go below 0 on archiving where the client state + // gets out of sync with the server state + total_count: Math.max(0, state.metadata.total_count - items.length), + unseen_count: Math.max(0, state.metadata.unseen_count - unseenCount), + unread_count: Math.max(0, state.metadata.unread_count - unreadCount), }; // Remove the archiving entries @@ -464,8 +472,15 @@ class Feed { async fetch(options: FetchFeedOptions = {}) { const { networkStatus, ...state } = this.store.getState(); + // If the user is not authenticated, then do nothing + if (!this.knock.isAuthenticated()) { + this.knock.log("[Feed] User is not authenticated, skipping fetch"); + return; + } + // If there's an existing request in flight, then do nothing if (isRequestInFlight(networkStatus)) { + this.knock.log("[Feed] Request is in flight, skipping fetch"); return; } @@ -750,7 +765,7 @@ class Feed { // If we're initializing but they have previously opted to listen to real-time updates // then we will automatically reconnect on their behalf - if (this.hasSubscribedToRealTimeUpdates) { + if (this.hasSubscribedToRealTimeUpdates && this.knock.isAuthenticated()) { if (!maybeSocket.isConnected()) maybeSocket.connect(); this.channel.join(); } diff --git a/packages/client/src/knock.ts b/packages/client/src/knock.ts index 90066575..2ef937a4 100644 --- a/packages/client/src/knock.ts +++ b/packages/client/src/knock.ts @@ -48,16 +48,6 @@ class Knock { } client() { - if (!this.userId) { - console.warn( - `[Knock] You must call authenticate(userId, userToken) first before trying to make a request. - Typically you'll see this message when you're creating a feed instance before having called - authenticate with a user Id and token. That means we won't know who to issue the request - to Knock on behalf of. - `, - ); - } - // Initiate a new API client if we don't have one yet if (!this.apiClient) { this.apiClient = this.createApiClient();