Skip to content

Commit 013ad8d

Browse files
authoredJan 10, 2025··
feat(KNO-7528): add MsTeamsAuthButton (#339)
1 parent a8b9569 commit 013ad8d

File tree

31 files changed

+797
-46
lines changed

31 files changed

+797
-46
lines changed
 

‎.changeset/khaki-ways-protect.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@knocklabs/react-core": minor
3+
"@knocklabs/client": minor
4+
"@knocklabs/react": minor
5+
---
6+
7+
feat: add MsTeamsAuthButton

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"build:packages": "turbo build --filter=\"./packages/*\"",
1010
"dev": "turbo dev",
1111
"dev:next-example": "turbo dev --filter=\"./packages/*\" --filter=nextjs-example",
12+
"dev:packages": "turbo dev --filter=\"./packages/*\"",
1213
"lint": "turbo lint",
1314
"format": "turbo format",
1415
"format:check": "turbo format:check",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ApiResponse } from "../../api";
2+
import { AuthCheckInput, RevokeAccessTokenInput } from "../../interfaces";
3+
import Knock from "../../knock";
4+
import { TENANT_OBJECT_COLLECTION } from "../objects/constants";
5+
6+
class MsTeamsClient {
7+
private instance: Knock;
8+
9+
constructor(instance: Knock) {
10+
this.instance = instance;
11+
}
12+
13+
async authCheck({ tenant: tenantId, knockChannelId }: AuthCheckInput) {
14+
const result = await this.instance.client().makeRequest({
15+
method: "GET",
16+
url: `/v1/providers/ms-teams/${knockChannelId}/auth_check`,
17+
params: {
18+
ms_teams_tenant_object: {
19+
object_id: tenantId,
20+
collection: TENANT_OBJECT_COLLECTION,
21+
},
22+
channel_id: knockChannelId,
23+
},
24+
});
25+
26+
return this.handleResponse(result);
27+
}
28+
29+
async revokeAccessToken({
30+
tenant: tenantId,
31+
knockChannelId,
32+
}: RevokeAccessTokenInput) {
33+
const result = await this.instance.client().makeRequest({
34+
method: "PUT",
35+
url: `/v1/providers/ms-teams/${knockChannelId}/revoke_access`,
36+
params: {
37+
ms_teams_tenant_object: {
38+
object_id: tenantId,
39+
collection: TENANT_OBJECT_COLLECTION,
40+
},
41+
channel_id: knockChannelId,
42+
},
43+
});
44+
45+
return this.handleResponse(result);
46+
}
47+
48+
private handleResponse(response: ApiResponse) {
49+
if (response.statusCode === "error") {
50+
if (response.error?.response?.status < 500) {
51+
return response.error || response.body;
52+
}
53+
throw new Error(response.error || response.body);
54+
}
55+
56+
return response.body;
57+
}
58+
}
59+
60+
export default MsTeamsClient;

‎packages/client/src/clients/slack/index.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { ApiResponse } from "../../api";
2+
import { AuthCheckInput, RevokeAccessTokenInput } from "../../interfaces";
23
import Knock from "../../knock";
4+
import { TENANT_OBJECT_COLLECTION } from "../objects/constants";
35

4-
import {
5-
AuthCheckInput,
6-
GetSlackChannelsInput,
7-
GetSlackChannelsResponse,
8-
RevokeAccessTokenInput,
9-
} from "./interfaces";
10-
11-
const TENANT_COLLECTION = "$tenants";
6+
import { GetSlackChannelsInput, GetSlackChannelsResponse } from "./interfaces";
127

138
class SlackClient {
149
private instance: Knock;
@@ -24,7 +19,7 @@ class SlackClient {
2419
params: {
2520
access_token_object: {
2621
object_id: tenant,
27-
collection: TENANT_COLLECTION,
22+
collection: TENANT_OBJECT_COLLECTION,
2823
},
2924
channel_id: knockChannelId,
3025
},
@@ -45,7 +40,7 @@ class SlackClient {
4540
params: {
4641
access_token_object: {
4742
object_id: tenant,
48-
collection: TENANT_COLLECTION,
43+
collection: TENANT_OBJECT_COLLECTION,
4944
},
5045
channel_id: knockChannelId,
5146
query_options: {
@@ -68,7 +63,7 @@ class SlackClient {
6863
params: {
6964
access_token_object: {
7065
object_id: tenant,
71-
collection: TENANT_COLLECTION,
66+
collection: TENANT_OBJECT_COLLECTION,
7267
},
7368
channel_id: knockChannelId,
7469
},

‎packages/client/src/clients/slack/interfaces.ts

-10
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,6 @@ export type GetSlackChannelsInput = {
1717
};
1818
};
1919

20-
export type AuthCheckInput = {
21-
tenant: string;
22-
knockChannelId: string;
23-
};
24-
25-
export type RevokeAccessTokenInput = {
26-
tenant: string;
27-
knockChannelId: string;
28-
};
29-
3020
export type GetSlackChannelsResponse = {
3121
slack_channels: SlackChannel[];
3222
next_cursor: string | null;

‎packages/client/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from "./clients/objects/constants";
99
export * from "./clients/preferences/interfaces";
1010
export * from "./clients/slack";
1111
export * from "./clients/slack/interfaces";
12+
export * from "./clients/ms-teams";
1213
export * from "./clients/users";
1314
export * from "./clients/users/interfaces";
1415
export * from "./clients/messages";

‎packages/client/src/interfaces.ts

+10
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,13 @@ export interface BulkOperation {
6666
inserted_at: string;
6767
updated_at: string;
6868
}
69+
70+
export type AuthCheckInput = {
71+
tenant: string;
72+
knockChannelId: string;
73+
};
74+
75+
export type RevokeAccessTokenInput = {
76+
tenant: string;
77+
knockChannelId: string;
78+
};

‎packages/client/src/knock.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { jwtDecode } from "jwt-decode";
33
import ApiClient from "./api";
44
import FeedClient from "./clients/feed";
55
import MessageClient from "./clients/messages";
6+
import MsTeamsClient from "./clients/ms-teams";
67
import ObjectClient from "./clients/objects";
78
import Preferences from "./clients/preferences";
89
import SlackClient from "./clients/slack";
@@ -27,6 +28,7 @@ class Knock {
2728
readonly objects = new ObjectClient(this);
2829
readonly preferences = new Preferences(this);
2930
readonly slack = new SlackClient(this);
31+
readonly msTeams = new MsTeamsClient(this);
3032
readonly user = new UserClient(this);
3133
readonly messages = new MessageClient(this);
3234

‎packages/react-core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./modules/core";
22
export * from "./modules/feed";
3+
export * from "./modules/ms-teams";
34
export * from "./modules/slack";
45
export * from "./modules/i18n";

‎packages/react-core/src/modules/core/utils.ts

+20
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,23 @@ export function slackProviderKey({
7474
.filter((f) => f !== null && f !== undefined)
7575
.join("-");
7676
}
77+
78+
/*
79+
Used to build a consistent key for the KnockMsTeamsProvider so that React knows when
80+
to trigger a re-render of the context when a key property changes.
81+
*/
82+
export function msTeamsProviderKey({
83+
knockMsTeamsChannelId,
84+
tenantId,
85+
connectionStatus,
86+
errorLabel,
87+
}: {
88+
knockMsTeamsChannelId: string;
89+
tenantId: string;
90+
connectionStatus: string;
91+
errorLabel: string | null;
92+
}) {
93+
return [knockMsTeamsChannelId, tenantId, connectionStatus, errorLabel]
94+
.filter((f) => f !== null && f !== undefined)
95+
.join("-");
96+
}

‎packages/react-core/src/modules/i18n/languages/en.ts

+10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ const en: I18nContent = {
1212
unread: "Unread",
1313
read: "Read",
1414
unseen: "Unseen",
15+
msTeamsConnect: "Connect to Microsoft Teams",
16+
msTeamsConnected: "Connected",
17+
msTeamsConnecting: "Connecting to Microsoft Teams…",
18+
msTeamsConnectContainerDescription:
19+
"Connect to get notifications in Microsoft Teams",
20+
msTeamsDisconnect: "Disconnect",
21+
msTeamsDisconnecting: "Disconnecting from Microsoft Teams…",
22+
msTeamsError: "Error",
23+
msTeamsReconnect: "Reconnect",
24+
msTeamsTenantIdNotSet: "Microsoft Teams tenant ID not set.",
1525
slackConnectChannel: "Connect channel",
1626
slackChannelId: "Slack channel ID",
1727
slackConnecting: "Connecting to Slack...",

‎packages/react-core/src/modules/i18n/languages/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ export interface Translations {
1313
readonly unread: string;
1414
readonly read: string;
1515
readonly unseen: string;
16+
readonly msTeamsConnect: string;
17+
readonly msTeamsConnected: string;
18+
readonly msTeamsConnecting: string;
19+
readonly msTeamsConnectContainerDescription: string;
20+
readonly msTeamsDisconnect: string;
21+
readonly msTeamsDisconnecting: string;
22+
readonly msTeamsError: string;
23+
readonly msTeamsReconnect: string;
24+
readonly msTeamsTenantIdNotSet: string;
1625
readonly slackConnectChannel: string;
1726
readonly slackChannelId: string;
1827
readonly slackConnecting: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as React from "react";
2+
import { PropsWithChildren } from "react";
3+
4+
import { useKnockClient } from "../../core";
5+
import { msTeamsProviderKey } from "../../core/utils";
6+
import { useMsTeamsConnectionStatus } from "../hooks";
7+
import { ConnectionStatus } from "../hooks/useMsTeamsConnectionStatus";
8+
9+
export interface KnockMsTeamsProviderState {
10+
knockMsTeamsChannelId: string;
11+
tenantId: string;
12+
connectionStatus: ConnectionStatus;
13+
setConnectionStatus: (connectionStatus: ConnectionStatus) => void;
14+
errorLabel: string | null;
15+
setErrorLabel: (label: string) => void;
16+
actionLabel: string | null;
17+
setActionLabel: (label: string | null) => void;
18+
}
19+
20+
const MsTeamsProviderStateContext =
21+
React.createContext<KnockMsTeamsProviderState | null>(null);
22+
23+
export interface KnockMsTeamsProviderProps {
24+
knockMsTeamsChannelId: string;
25+
tenantId: string;
26+
}
27+
28+
export const KnockMsTeamsProvider: React.FC<
29+
PropsWithChildren<KnockMsTeamsProviderProps>
30+
> = ({ knockMsTeamsChannelId, tenantId, children }) => {
31+
const knock = useKnockClient();
32+
33+
const {
34+
connectionStatus,
35+
setConnectionStatus,
36+
errorLabel,
37+
setErrorLabel,
38+
actionLabel,
39+
setActionLabel,
40+
} = useMsTeamsConnectionStatus(knock, knockMsTeamsChannelId, tenantId);
41+
42+
return (
43+
<MsTeamsProviderStateContext.Provider
44+
key={msTeamsProviderKey({
45+
knockMsTeamsChannelId,
46+
tenantId,
47+
connectionStatus,
48+
errorLabel,
49+
})}
50+
value={{
51+
connectionStatus,
52+
setConnectionStatus,
53+
errorLabel,
54+
setErrorLabel,
55+
actionLabel,
56+
setActionLabel,
57+
knockMsTeamsChannelId,
58+
tenantId,
59+
}}
60+
>
61+
{children}
62+
</MsTeamsProviderStateContext.Provider>
63+
);
64+
};
65+
66+
export const useKnockMsTeamsClient = (): KnockMsTeamsProviderState => {
67+
const context = React.useContext(MsTeamsProviderStateContext);
68+
if (!context) {
69+
throw new Error(
70+
"useKnockMsTeamsClient must be used within a KnockMsTeamsProvider",
71+
);
72+
}
73+
return context;
74+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./KnockMsTeamsProvider";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as useMsTeamsConnectionStatus } from "./useMsTeamsConnectionStatus";
2+
export { default as useMsTeamsAuth } from "./useMsTeamsAuth";

0 commit comments

Comments
 (0)
Please sign in to comment.