From e6c66526b0db069d7b439159ee8169af841a6ce7 Mon Sep 17 00:00:00 2001 From: Meryl Dakin Date: Wed, 6 May 2026 16:25:53 -0400 Subject: [PATCH 1/5] update --- examples/slack-connect-example/.env.sample | 4 ++ .../pages/api/set_token.ts | 6 ++- .../slack-connect-example/pages/index.tsx | 47 ++++++++++++++++++- .../core/hooks/useAuthPostMessageListener.ts | 5 ++ .../hooks/useAuthPostMessageListener.test.ts | 44 +++++++++++++++++ 5 files changed, 103 insertions(+), 3 deletions(-) diff --git a/examples/slack-connect-example/.env.sample b/examples/slack-connect-example/.env.sample index d2bac8053..4d90798aa 100644 --- a/examples/slack-connect-example/.env.sample +++ b/examples/slack-connect-example/.env.sample @@ -21,4 +21,8 @@ NEXT_PUBLIC_SLACK_CLIENT_ID= NEXT_PUBLIC_KNOCK_SLACK_CHANNEL_ID= NEXT_PUBLIC_REDIRECT_URL=http://localhost:3000/ +# MS Teams configuration (optional — enables side-by-side testing with Slack) +NEXT_PUBLIC_GRAPH_API_CLIENT_ID= +NEXT_PUBLIC_KNOCK_MS_TEAMS_CHANNEL_ID= + NEXT_PUBLIC_KNOCK_BRANCH= diff --git a/examples/slack-connect-example/pages/api/set_token.ts b/examples/slack-connect-example/pages/api/set_token.ts index dfbb28e14..c308d8e12 100644 --- a/examples/slack-connect-example/pages/api/set_token.ts +++ b/examples/slack-connect-example/pages/api/set_token.ts @@ -14,7 +14,10 @@ export default async function handler( const { tenant, user, slackChannelsRecipientObject } = req.body; try { - const signingKey = process.env.KNOCK_SIGNING_KEY!; + const rawKey = process.env.KNOCK_SIGNING_KEY!; + const signingKey = rawKey.startsWith("LS0t") + ? Buffer.from(rawKey, "base64").toString("utf-8") + : rawKey; // JWT NumericDates specified in seconds: const currentTime = Math.floor(Date.now() / 1000); @@ -30,6 +33,7 @@ export default async function handler( grants: { [`https://api.knock.app/v1/objects/$tenants/${tenant}`]: { "slack/channels_read": [{}], + "ms_teams/channels_read": [{}], }, [`https://api.knock.app/v1/objects/${slackChannelsRecipientObject.collection}/${slackChannelsRecipientObject.objectId}`]: { diff --git a/examples/slack-connect-example/pages/index.tsx b/examples/slack-connect-example/pages/index.tsx index 29e99172c..9944fea02 100644 --- a/examples/slack-connect-example/pages/index.tsx +++ b/examples/slack-connect-example/pages/index.tsx @@ -1,6 +1,8 @@ import { + KnockMsTeamsProvider, KnockProvider, KnockSlackProvider, + MsTeamsAuthButton, SlackAuthButton, SlackAuthContainer, SlackChannelCombobox, @@ -26,10 +28,14 @@ export default function Home() { collection: process.env.NEXT_PUBLIC_CONNECTIONS_COLLECTION!, }; - const onAuthComplete = (result: string) => { + const onSlackAuthComplete = (result: string) => { console.log("Result from Slack authentication:", result); }; + const onTeamsAuthComplete = (result: string) => { + console.log("Result from MS Teams authentication:", result); + }; + const { isLoading, isError } = useSetToken({ tenant, user, @@ -114,7 +120,7 @@ export default function Home() { @@ -146,6 +152,43 @@ export default function Home() { + + {/* MS Teams provider rendered as sibling to reproduce KNO-13032 */} + {process.env.NEXT_PUBLIC_KNOCK_MS_TEAMS_CHANNEL_ID && ( + +
+
+ MS Teams connector +
+
+ +
+
+
+ )} ); } diff --git a/packages/react-core/src/modules/core/hooks/useAuthPostMessageListener.ts b/packages/react-core/src/modules/core/hooks/useAuthPostMessageListener.ts index cc3cbfaa5..9166a8115 100644 --- a/packages/react-core/src/modules/core/hooks/useAuthPostMessageListener.ts +++ b/packages/react-core/src/modules/core/hooks/useAuthPostMessageListener.ts @@ -84,6 +84,11 @@ export function useAuthPostMessageListener( return; } + // Ignore messages when this integration hasn't opened a popup + if (!popupWindowRef.current || popupWindowRef.current.closed) { + return; + } + const messageType = getMessageType(event.data); if (messageType === "authComplete") { diff --git a/packages/react-core/test/core/hooks/useAuthPostMessageListener.test.ts b/packages/react-core/test/core/hooks/useAuthPostMessageListener.test.ts index dfaed2a79..9007d72ba 100644 --- a/packages/react-core/test/core/hooks/useAuthPostMessageListener.test.ts +++ b/packages/react-core/test/core/hooks/useAuthPostMessageListener.test.ts @@ -132,6 +132,50 @@ describe("useAuthPostMessageListener", () => { expect(popupWindowRef.current).toBeNull(); }); + it("should ignore messages when popupWindowRef is null", () => { + popupWindowRef.current = null; + + renderHook(() => + useAuthPostMessageListener({ + knockHost, + popupWindowRef, + setConnectionStatus, + onAuthenticationComplete, + }), + ); + + const event = new MessageEvent("message", { + data: "authComplete", + origin: knockHost, + }); + window.dispatchEvent(event); + + expect(setConnectionStatus).not.toHaveBeenCalled(); + expect(onAuthenticationComplete).not.toHaveBeenCalled(); + }); + + it("should ignore messages when popup is closed", () => { + mockPopup.closed = true; + + renderHook(() => + useAuthPostMessageListener({ + knockHost, + popupWindowRef, + setConnectionStatus, + onAuthenticationComplete, + }), + ); + + const event = new MessageEvent("message", { + data: "authComplete", + origin: knockHost, + }); + window.dispatchEvent(event); + + expect(setConnectionStatus).not.toHaveBeenCalled(); + expect(onAuthenticationComplete).not.toHaveBeenCalled(); + }); + it("should ignore messages from different origins", () => { renderHook(() => useAuthPostMessageListener({ From 0b75ea6115b16b73899f358a9925600d4828d649 Mon Sep 17 00:00:00 2001 From: Meryl Dakin Date: Wed, 6 May 2026 16:29:16 -0400 Subject: [PATCH 2/5] update --- examples/slack-connect-example/pages/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/slack-connect-example/pages/index.tsx b/examples/slack-connect-example/pages/index.tsx index 9944fea02..b65f3ab5b 100644 --- a/examples/slack-connect-example/pages/index.tsx +++ b/examples/slack-connect-example/pages/index.tsx @@ -179,9 +179,7 @@ export default function Home() {
From 083dd7fe478f0017a4771e30c39f7a23ebe0a2c6 Mon Sep 17 00:00:00 2001 From: Meryl Dakin Date: Wed, 6 May 2026 16:30:12 -0400 Subject: [PATCH 3/5] update --- examples/slack-connect-example/.env.sample | 4 -- .../pages/api/set_token.ts | 6 +-- .../slack-connect-example/pages/index.tsx | 45 +------------------ 3 files changed, 3 insertions(+), 52 deletions(-) diff --git a/examples/slack-connect-example/.env.sample b/examples/slack-connect-example/.env.sample index 4d90798aa..d2bac8053 100644 --- a/examples/slack-connect-example/.env.sample +++ b/examples/slack-connect-example/.env.sample @@ -21,8 +21,4 @@ NEXT_PUBLIC_SLACK_CLIENT_ID= NEXT_PUBLIC_KNOCK_SLACK_CHANNEL_ID= NEXT_PUBLIC_REDIRECT_URL=http://localhost:3000/ -# MS Teams configuration (optional — enables side-by-side testing with Slack) -NEXT_PUBLIC_GRAPH_API_CLIENT_ID= -NEXT_PUBLIC_KNOCK_MS_TEAMS_CHANNEL_ID= - NEXT_PUBLIC_KNOCK_BRANCH= diff --git a/examples/slack-connect-example/pages/api/set_token.ts b/examples/slack-connect-example/pages/api/set_token.ts index c308d8e12..dfbb28e14 100644 --- a/examples/slack-connect-example/pages/api/set_token.ts +++ b/examples/slack-connect-example/pages/api/set_token.ts @@ -14,10 +14,7 @@ export default async function handler( const { tenant, user, slackChannelsRecipientObject } = req.body; try { - const rawKey = process.env.KNOCK_SIGNING_KEY!; - const signingKey = rawKey.startsWith("LS0t") - ? Buffer.from(rawKey, "base64").toString("utf-8") - : rawKey; + const signingKey = process.env.KNOCK_SIGNING_KEY!; // JWT NumericDates specified in seconds: const currentTime = Math.floor(Date.now() / 1000); @@ -33,7 +30,6 @@ export default async function handler( grants: { [`https://api.knock.app/v1/objects/$tenants/${tenant}`]: { "slack/channels_read": [{}], - "ms_teams/channels_read": [{}], }, [`https://api.knock.app/v1/objects/${slackChannelsRecipientObject.collection}/${slackChannelsRecipientObject.objectId}`]: { diff --git a/examples/slack-connect-example/pages/index.tsx b/examples/slack-connect-example/pages/index.tsx index b65f3ab5b..29e99172c 100644 --- a/examples/slack-connect-example/pages/index.tsx +++ b/examples/slack-connect-example/pages/index.tsx @@ -1,8 +1,6 @@ import { - KnockMsTeamsProvider, KnockProvider, KnockSlackProvider, - MsTeamsAuthButton, SlackAuthButton, SlackAuthContainer, SlackChannelCombobox, @@ -28,14 +26,10 @@ export default function Home() { collection: process.env.NEXT_PUBLIC_CONNECTIONS_COLLECTION!, }; - const onSlackAuthComplete = (result: string) => { + const onAuthComplete = (result: string) => { console.log("Result from Slack authentication:", result); }; - const onTeamsAuthComplete = (result: string) => { - console.log("Result from MS Teams authentication:", result); - }; - const { isLoading, isError } = useSetToken({ tenant, user, @@ -120,7 +114,7 @@ export default function Home() {
@@ -152,41 +146,6 @@ export default function Home() { - - {/* MS Teams provider rendered as sibling to reproduce KNO-13032 */} - {process.env.NEXT_PUBLIC_KNOCK_MS_TEAMS_CHANNEL_ID && ( - -
-
- MS Teams connector -
-
- -
-
-
- )} ); } From 377e3d167b40f40d25bfa14e2d93a45c4d141f2b Mon Sep 17 00:00:00 2001 From: Meryl Dakin Date: Wed, 6 May 2026 16:33:08 -0400 Subject: [PATCH 4/5] update --- .changeset/fix-auth-button-cross-contamination.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-auth-button-cross-contamination.md diff --git a/.changeset/fix-auth-button-cross-contamination.md b/.changeset/fix-auth-button-cross-contamination.md new file mode 100644 index 000000000..64fd6e95f --- /dev/null +++ b/.changeset/fix-auth-button-cross-contamination.md @@ -0,0 +1,5 @@ +--- +"@knocklabs/react-core": patch +--- + +Fix auth button cross-contamination when SlackKit and TeamsKit are rendered simultaneously. The shared `useAuthPostMessageListener` hook now checks whether its own popup is open before processing `authComplete` messages, preventing one integration's OAuth completion from incorrectly updating the other's connection state. From a5c3c44c1f3ed2274a60ac8b2707a0da133384e6 Mon Sep 17 00:00:00 2001 From: Meryl Dakin Date: Wed, 6 May 2026 16:40:36 -0400 Subject: [PATCH 5/5] update --- .../core/hooks/useAuthPostMessageListener.ts | 2 +- .../hooks/useAuthPostMessageListener.test.ts | 22 ------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/packages/react-core/src/modules/core/hooks/useAuthPostMessageListener.ts b/packages/react-core/src/modules/core/hooks/useAuthPostMessageListener.ts index 9166a8115..45c62ff65 100644 --- a/packages/react-core/src/modules/core/hooks/useAuthPostMessageListener.ts +++ b/packages/react-core/src/modules/core/hooks/useAuthPostMessageListener.ts @@ -85,7 +85,7 @@ export function useAuthPostMessageListener( } // Ignore messages when this integration hasn't opened a popup - if (!popupWindowRef.current || popupWindowRef.current.closed) { + if (!popupWindowRef.current) { return; } diff --git a/packages/react-core/test/core/hooks/useAuthPostMessageListener.test.ts b/packages/react-core/test/core/hooks/useAuthPostMessageListener.test.ts index 9007d72ba..994d383e8 100644 --- a/packages/react-core/test/core/hooks/useAuthPostMessageListener.test.ts +++ b/packages/react-core/test/core/hooks/useAuthPostMessageListener.test.ts @@ -154,28 +154,6 @@ describe("useAuthPostMessageListener", () => { expect(onAuthenticationComplete).not.toHaveBeenCalled(); }); - it("should ignore messages when popup is closed", () => { - mockPopup.closed = true; - - renderHook(() => - useAuthPostMessageListener({ - knockHost, - popupWindowRef, - setConnectionStatus, - onAuthenticationComplete, - }), - ); - - const event = new MessageEvent("message", { - data: "authComplete", - origin: knockHost, - }); - window.dispatchEvent(event); - - expect(setConnectionStatus).not.toHaveBeenCalled(); - expect(onAuthenticationComplete).not.toHaveBeenCalled(); - }); - it("should ignore messages from different origins", () => { renderHook(() => useAuthPostMessageListener({