From 110ae580bc4b75cea040727572b3dde2e1c2f25c Mon Sep 17 00:00:00 2001 From: Matthew Mikolay Date: Mon, 6 Jan 2025 10:35:50 -0500 Subject: [PATCH 01/17] Add MSTeamsAuthButton --- package.json | 1 + packages/client/src/clients/msTeams/index.ts | 59 ++++++++ .../client/src/clients/msTeams/interfaces.ts | 9 ++ packages/client/src/index.ts | 2 + packages/client/src/knock.ts | 2 + packages/react-core/src/index.ts | 1 + packages/react-core/src/modules/core/utils.ts | 20 +++ .../src/modules/i18n/languages/en.ts | 10 ++ .../src/modules/i18n/languages/index.ts | 9 ++ .../ms-teams/context/KnockMSTeamsProvider.tsx | 74 ++++++++++ .../src/modules/ms-teams/context/index.ts | 1 + .../src/modules/ms-teams/hooks/index.ts | 2 + .../modules/ms-teams/hooks/useMSTeamsAuth.ts | 83 +++++++++++ .../hooks/useMSTeamsConnectionStatus.ts | 82 +++++++++++ .../react-core/src/modules/ms-teams/index.ts | 2 + packages/react/src/index.ts | 1 + packages/react/src/modules/core/utils.ts | 22 +++ .../MSTeamsAuthButton/MSTeamsAuthButton.tsx | 136 ++++++++++++++++++ .../components/MSTeamsAuthButton/index.ts | 1 + .../components/MSTeamsAuthButton/styles.css | 63 ++++++++ .../MSTeamsAuthContainer.tsx | 30 ++++ .../components/MSTeamsAuthContainer/index.ts | 1 + .../MSTeamsAuthContainer/styles.css | 28 ++++ .../components/MSTeamsIcon/MSTeamsIcon.tsx | 84 +++++++++++ .../ms-teams/components/MSTeamsIcon/index.ts | 1 + packages/react/src/modules/ms-teams/index.ts | 2 + packages/react/src/modules/ms-teams/theme.css | 54 +++++++ .../SlackAuthButton/SlackAuthButton.tsx | 28 +--- yarn.lock | 19 +++ 29 files changed, 802 insertions(+), 25 deletions(-) create mode 100644 packages/client/src/clients/msTeams/index.ts create mode 100644 packages/client/src/clients/msTeams/interfaces.ts create mode 100644 packages/react-core/src/modules/ms-teams/context/KnockMSTeamsProvider.tsx create mode 100644 packages/react-core/src/modules/ms-teams/context/index.ts create mode 100644 packages/react-core/src/modules/ms-teams/hooks/index.ts create mode 100644 packages/react-core/src/modules/ms-teams/hooks/useMSTeamsAuth.ts create mode 100644 packages/react-core/src/modules/ms-teams/hooks/useMSTeamsConnectionStatus.ts create mode 100644 packages/react-core/src/modules/ms-teams/index.ts create mode 100644 packages/react/src/modules/core/utils.ts create mode 100644 packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/MSTeamsAuthButton.tsx create mode 100644 packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/index.ts create mode 100644 packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/styles.css create mode 100644 packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/MSTeamsAuthContainer.tsx create mode 100644 packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/index.ts create mode 100644 packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/styles.css create mode 100644 packages/react/src/modules/ms-teams/components/MSTeamsIcon/MSTeamsIcon.tsx create mode 100644 packages/react/src/modules/ms-teams/components/MSTeamsIcon/index.ts create mode 100644 packages/react/src/modules/ms-teams/index.ts create mode 100644 packages/react/src/modules/ms-teams/theme.css diff --git a/package.json b/package.json index fef248df..89eec117 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build:packages": "turbo build --filter=\"./packages/*\"", "dev": "turbo dev", "dev:next-example": "turbo dev --filter=\"./packages/*\" --filter=nextjs-example", + "dev:packages": "turbo dev --filter=\"./packages/*\"", "lint": "turbo lint", "format": "turbo format", "format:check": "turbo format:check", diff --git a/packages/client/src/clients/msTeams/index.ts b/packages/client/src/clients/msTeams/index.ts new file mode 100644 index 00000000..f11ad09a --- /dev/null +++ b/packages/client/src/clients/msTeams/index.ts @@ -0,0 +1,59 @@ +import { ApiResponse } from "../../api"; +import Knock from "../../knock"; + +import { MSTeamsAuthCheckInput, MSTeamsDisconnectInput } from "./interfaces"; + +const TENANT_COLLECTION = "$tenants"; + +class MSTeamsClient { + private instance: Knock; + + constructor(instance: Knock) { + this.instance = instance; + } + + async authCheck({ tenantId, knockChannelId }: MSTeamsAuthCheckInput) { + const result = await this.instance.client().makeRequest({ + method: "GET", + url: `/v1/providers/ms-teams/${knockChannelId}/auth_check`, + params: { + ms_teams_tenant_object: { + object_id: tenantId, + collection: TENANT_COLLECTION, + }, + channel_id: knockChannelId, + }, + }); + + return this.handleResponse(result); + } + + async disconnect({ tenantId, knockChannelId }: MSTeamsDisconnectInput) { + const result = await this.instance.client().makeRequest({ + method: "PUT", + url: `/v1/providers/ms-teams/${knockChannelId}/disconnect`, + params: { + ms_teams_tenant_object: { + object_id: tenantId, + collection: TENANT_COLLECTION, + }, + channel_id: knockChannelId, + }, + }); + + return this.handleResponse(result); + } + + private handleResponse(response: ApiResponse) { + if (response.statusCode === "error") { + if (response.error?.response?.status < 500) { + return response.error || response.body; + } + throw new Error(response.error || response.body); + } + + return response.body; + } +} + +export default MSTeamsClient; diff --git a/packages/client/src/clients/msTeams/interfaces.ts b/packages/client/src/clients/msTeams/interfaces.ts new file mode 100644 index 00000000..ac596fa7 --- /dev/null +++ b/packages/client/src/clients/msTeams/interfaces.ts @@ -0,0 +1,9 @@ +export type MSTeamsAuthCheckInput = { + tenantId: string; + knockChannelId: string; +}; + +export type MSTeamsDisconnectInput = { + tenantId: string; + knockChannelId: string; +}; diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index aceedc96..6bb53034 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -9,6 +9,8 @@ export * from "./clients/objects/constants"; export * from "./clients/preferences/interfaces"; export * from "./clients/slack"; export * from "./clients/slack/interfaces"; +export * from "./clients/msTeams"; +export * from "./clients/msTeams/interfaces"; export * from "./clients/users"; export * from "./clients/users/interfaces"; export * from "./clients/messages"; diff --git a/packages/client/src/knock.ts b/packages/client/src/knock.ts index 2ef937a4..05c90114 100644 --- a/packages/client/src/knock.ts +++ b/packages/client/src/knock.ts @@ -3,6 +3,7 @@ import { jwtDecode } from "jwt-decode"; import ApiClient from "./api"; import FeedClient from "./clients/feed"; import MessageClient from "./clients/messages"; +import MSTeamsClient from "./clients/msTeams"; import ObjectClient from "./clients/objects"; import Preferences from "./clients/preferences"; import SlackClient from "./clients/slack"; @@ -27,6 +28,7 @@ class Knock { readonly objects = new ObjectClient(this); readonly preferences = new Preferences(this); readonly slack = new SlackClient(this); + readonly msTeams = new MSTeamsClient(this); readonly user = new UserClient(this); readonly messages = new MessageClient(this); diff --git a/packages/react-core/src/index.ts b/packages/react-core/src/index.ts index 6ea0cb7b..57f0578a 100644 --- a/packages/react-core/src/index.ts +++ b/packages/react-core/src/index.ts @@ -1,4 +1,5 @@ export * from "./modules/core"; export * from "./modules/feed"; +export * from "./modules/ms-teams"; export * from "./modules/slack"; export * from "./modules/i18n"; diff --git a/packages/react-core/src/modules/core/utils.ts b/packages/react-core/src/modules/core/utils.ts index 06a19052..5bc93e23 100644 --- a/packages/react-core/src/modules/core/utils.ts +++ b/packages/react-core/src/modules/core/utils.ts @@ -74,3 +74,23 @@ export function slackProviderKey({ .filter((f) => f !== null && f !== undefined) .join("-"); } + +/* + Used to build a consistent key for the KnockMSTeamsProvider so that React knows when + to trigger a re-render of the context when a key property changes. +*/ +export function msTeamsProviderKey({ + knockMSTeamsChannelId, + tenantId, + connectionStatus, + errorLabel, +}: { + knockMSTeamsChannelId: string; + tenantId: string; + connectionStatus: string; + errorLabel: string | null; +}) { + return [knockMSTeamsChannelId, tenantId, connectionStatus, errorLabel] + .filter((f) => f !== null && f !== undefined) + .join("-"); +} diff --git a/packages/react-core/src/modules/i18n/languages/en.ts b/packages/react-core/src/modules/i18n/languages/en.ts index 8f9f5de6..bc79b0e8 100644 --- a/packages/react-core/src/modules/i18n/languages/en.ts +++ b/packages/react-core/src/modules/i18n/languages/en.ts @@ -12,6 +12,16 @@ const en: I18nContent = { unread: "Unread", read: "Read", unseen: "Unseen", + msTeamsConnect: "Connect to Microsoft Teams", + msTeamsConnected: "Connected", + msTeamsConnecting: "Connecting to Microsoft Teams…", + msTeamsConnectContainerDescription: + "Connect to get notifications in Microsoft Teams", + msTeamsDisconnect: "Disconnect", + msTeamsDisconnecting: "Disconnecting from Microsoft Teams…", + msTeamsError: "Error", + msTeamsReconnect: "Reconnect", + msTeamsTenantIdNotSet: "Microsoft Teams tenant ID not set.", slackConnectChannel: "Connect channel", slackChannelId: "Slack channel ID", slackConnecting: "Connecting to Slack...", diff --git a/packages/react-core/src/modules/i18n/languages/index.ts b/packages/react-core/src/modules/i18n/languages/index.ts index 689065ab..9d70931f 100644 --- a/packages/react-core/src/modules/i18n/languages/index.ts +++ b/packages/react-core/src/modules/i18n/languages/index.ts @@ -13,6 +13,15 @@ export interface Translations { readonly unread: string; readonly read: string; readonly unseen: string; + readonly msTeamsConnect: string; + readonly msTeamsConnected: string; + readonly msTeamsConnecting: string; + readonly msTeamsConnectContainerDescription: string; + readonly msTeamsDisconnect: string; + readonly msTeamsDisconnecting: string; + readonly msTeamsError: string; + readonly msTeamsReconnect: string; + readonly msTeamsTenantIdNotSet: string; readonly slackConnectChannel: string; readonly slackChannelId: string; readonly slackConnecting: string; diff --git a/packages/react-core/src/modules/ms-teams/context/KnockMSTeamsProvider.tsx b/packages/react-core/src/modules/ms-teams/context/KnockMSTeamsProvider.tsx new file mode 100644 index 00000000..37982c1f --- /dev/null +++ b/packages/react-core/src/modules/ms-teams/context/KnockMSTeamsProvider.tsx @@ -0,0 +1,74 @@ +import * as React from "react"; +import { PropsWithChildren } from "react"; + +import { useKnockClient } from "../../core"; +import { msTeamsProviderKey } from "../../core/utils"; +import { useMSTeamsConnectionStatus } from "../hooks"; +import { ConnectionStatus } from "../hooks/useMSTeamsConnectionStatus"; + +export interface KnockMSTeamsProviderState { + knockMSTeamsChannelId: string; + tenantId: string; + connectionStatus: ConnectionStatus; + setConnectionStatus: (connectionStatus: ConnectionStatus) => void; + errorLabel: string | null; + setErrorLabel: (label: string) => void; + actionLabel: string | null; + setActionLabel: (label: string | null) => void; +} + +const MSTeamsProviderStateContext = + React.createContext(null); + +export interface KnockMSTeamsProviderProps { + knockMSTeamsChannelId: string; + tenantId: string; +} + +export const KnockMSTeamsProvider: React.FC< + PropsWithChildren +> = ({ knockMSTeamsChannelId, tenantId, children }) => { + const knock = useKnockClient(); + + const { + connectionStatus, + setConnectionStatus, + errorLabel, + setErrorLabel, + actionLabel, + setActionLabel, + } = useMSTeamsConnectionStatus(knock, knockMSTeamsChannelId, tenantId); + + return ( + + {children} + + ); +}; + +export const useKnockMSTeamsClient = (): KnockMSTeamsProviderState => { + const context = React.useContext(MSTeamsProviderStateContext); + if (!context) { + throw new Error( + "useKnockMSTeamsClient must be used within a KnockMSTeamsProvider", + ); + } + return context; +}; diff --git a/packages/react-core/src/modules/ms-teams/context/index.ts b/packages/react-core/src/modules/ms-teams/context/index.ts new file mode 100644 index 00000000..6fbe50bf --- /dev/null +++ b/packages/react-core/src/modules/ms-teams/context/index.ts @@ -0,0 +1 @@ +export * from "./KnockMSTeamsProvider"; diff --git a/packages/react-core/src/modules/ms-teams/hooks/index.ts b/packages/react-core/src/modules/ms-teams/hooks/index.ts new file mode 100644 index 00000000..b4444e13 --- /dev/null +++ b/packages/react-core/src/modules/ms-teams/hooks/index.ts @@ -0,0 +1,2 @@ +export { default as useMSTeamsConnectionStatus } from "./useMSTeamsConnectionStatus"; +export { default as useMSTeamsAuth } from "./useMSTeamsAuth"; diff --git a/packages/react-core/src/modules/ms-teams/hooks/useMSTeamsAuth.ts b/packages/react-core/src/modules/ms-teams/hooks/useMSTeamsAuth.ts new file mode 100644 index 00000000..52a57a75 --- /dev/null +++ b/packages/react-core/src/modules/ms-teams/hooks/useMSTeamsAuth.ts @@ -0,0 +1,83 @@ +import { useKnockMSTeamsClient } from ".."; +import { TENANT_OBJECT_COLLECTION } from "@knocklabs/client"; +import { useCallback } from "react"; + +import { useKnockClient } from "../../core"; + +const MS_TEAMS_ADMINCONSENT_URL = + "https://login.microsoftonline.com/organizations/adminconsent"; + +// @ts-expect-error env vars are statically replaced by Vite at build time +const REDIRECT_URI = import.meta.env.VITE_MS_TEAMS_REDIRECT_URI; + +interface UseMSTeamsAuthOutput { + buildMSTeamsAuthUrl: () => string; + disconnectFromMSTeams: () => void; +} + +function useMSTeamsAuth( + msTeamsBotId: string, + redirectUrl?: string, +): UseMSTeamsAuthOutput { + const knock = useKnockClient(); + const { + setConnectionStatus, + knockMSTeamsChannelId, + tenantId, + setActionLabel, + } = useKnockMSTeamsClient(); + + const buildMSTeamsAuthUrl = useCallback(() => { + const rawParams = { + state: JSON.stringify({ + redirect_url: redirectUrl, + ms_teams_tenant_object: { + object_id: tenantId, + collection: TENANT_OBJECT_COLLECTION, + }, + channel_id: knockMSTeamsChannelId, + public_key: knock.apiKey, + user_token: knock.userToken, + }), + client_id: msTeamsBotId, + redirect_uri: REDIRECT_URI, + }; + return `${MS_TEAMS_ADMINCONSENT_URL}?${new URLSearchParams(rawParams)}`; + }, [ + redirectUrl, + tenantId, + knockMSTeamsChannelId, + knock.apiKey, + knock.userToken, + msTeamsBotId, + ]); + + const disconnectFromMSTeams = useCallback(async () => { + setActionLabel(null); + setConnectionStatus("disconnecting"); + try { + const disconnectResult = await knock.msTeams.disconnect({ + tenantId, + knockChannelId: knockMSTeamsChannelId, + }); + + if (disconnectResult === "ok") { + setConnectionStatus("disconnected"); + } else { + setConnectionStatus("error"); + } + } catch (_error) { + setConnectionStatus("error"); + } + }, [ + setConnectionStatus, + knock.msTeams, + tenantId, + knockMSTeamsChannelId, + setActionLabel, + ]); + + return { buildMSTeamsAuthUrl, disconnectFromMSTeams }; +} + +export default useMSTeamsAuth; diff --git a/packages/react-core/src/modules/ms-teams/hooks/useMSTeamsConnectionStatus.ts b/packages/react-core/src/modules/ms-teams/hooks/useMSTeamsConnectionStatus.ts new file mode 100644 index 00000000..df803677 --- /dev/null +++ b/packages/react-core/src/modules/ms-teams/hooks/useMSTeamsConnectionStatus.ts @@ -0,0 +1,82 @@ +import Knock from "@knocklabs/client"; +import { useEffect, useState } from "react"; + +import { useTranslations } from "../../i18n"; + +export type ConnectionStatus = + | "connecting" + | "connected" + | "disconnected" + | "error" + | "disconnecting"; + +type UseMSTeamsConnectionStatusOutput = { + connectionStatus: ConnectionStatus; + setConnectionStatus: (status: ConnectionStatus) => void; + errorLabel: string | null; + setErrorLabel: (errorLabel: string) => void; + actionLabel: string | null; + setActionLabel: (actionLabel: string | null) => void; +}; + +function useMSTeamsConnectionStatus( + knock: Knock, + knockMSTeamsChannelId: string, + tenantId: string, +): UseMSTeamsConnectionStatusOutput { + const { t } = useTranslations(); + + const [connectionStatus, setConnectionStatus] = + useState("connecting"); + const [errorLabel, setErrorLabel] = useState(null); + const [actionLabel, setActionLabel] = useState(null); + + useEffect(() => { + const checkAuthStatus = async () => { + if (connectionStatus !== "connecting") return; + + try { + const authRes = await knock.msTeams.authCheck({ + tenantId, + knockChannelId: knockMSTeamsChannelId, + }); + + if (authRes.connection?.ok) { + return setConnectionStatus("connected"); + } + + if (!authRes.connection?.ok) { + return setConnectionStatus("disconnected"); + } + + // This is a normal response for a tenant that doesn't have an access + // token set on it, meaning it's not connected to MSTeams, so we + // give it a "disconnected" status instead of an error status. + if ( + authRes.code === "ERR_BAD_REQUEST" && + authRes.response?.data?.message === t("msTeamsTenantIdNotSet") + ) { + return setConnectionStatus("disconnected"); + } + + // This is for any Knock errors that would require a reconnect. + setConnectionStatus("error"); + } catch (_error) { + setConnectionStatus("error"); + } + }; + + checkAuthStatus(); + }, [connectionStatus, tenantId, knockMSTeamsChannelId, knock.msTeams, t]); + + return { + connectionStatus, + setConnectionStatus, + errorLabel, + setErrorLabel, + actionLabel, + setActionLabel, + }; +} + +export default useMSTeamsConnectionStatus; diff --git a/packages/react-core/src/modules/ms-teams/index.ts b/packages/react-core/src/modules/ms-teams/index.ts new file mode 100644 index 00000000..493207ae --- /dev/null +++ b/packages/react-core/src/modules/ms-teams/index.ts @@ -0,0 +1,2 @@ +export * from "./context"; +export * from "./hooks"; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 54cb3ef3..f746f1ec 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,5 +2,6 @@ import "./theme.css"; export * from "./modules/core"; export * from "./modules/feed"; +export * from "./modules/ms-teams"; export * from "./modules/slack"; export * from "@knocklabs/react-core"; diff --git a/packages/react/src/modules/core/utils.ts b/packages/react/src/modules/core/utils.ts new file mode 100644 index 00000000..01f53c40 --- /dev/null +++ b/packages/react/src/modules/core/utils.ts @@ -0,0 +1,22 @@ +export const openPopupWindow = (url: string) => { + const width = 600; + const height = 800; + // Calculate the position to center the window + const screenLeft = window.screenLeft ?? window.screenX; + const screenTop = window.screenTop ?? window.screenY; + + const innerWidth = + window.innerWidth ?? document.documentElement.clientWidth ?? screen.width; + const innerHeight = + window.innerHeight ?? + document.documentElement.clientHeight ?? + screen.height; + + const left = innerWidth / 2 - width / 2 + screenLeft; + const top = innerHeight / 2 - height / 2 + screenTop; + + // Window features + const features = `width=${width},height=${height},top=${top},left=${left}`; + + window.open(url, "_blank", features); +}; diff --git a/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/MSTeamsAuthButton.tsx b/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/MSTeamsAuthButton.tsx new file mode 100644 index 00000000..4f46ca9c --- /dev/null +++ b/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/MSTeamsAuthButton.tsx @@ -0,0 +1,136 @@ +import { + useKnockClient, + useKnockMSTeamsClient, + useMSTeamsAuth, + useTranslations, +} from "@knocklabs/react-core"; +import { FunctionComponent, useEffect } from "react"; + +import { openPopupWindow } from "../../../core/utils"; +import { MSTeamsIcon } from "../MSTeamsIcon"; + +import "./styles.css"; + +export interface MSTeamsAuthButtonProps { + msTeamsBotId: string; + redirectUrl?: string; + onAuthenticationComplete?: (authenticationResp: string) => void; +} + +export const MSTeamsAuthButton: FunctionComponent = ({ + msTeamsBotId, + redirectUrl, + onAuthenticationComplete, +}) => { + const { t } = useTranslations(); + const knock = useKnockClient(); + + const { + setConnectionStatus, + connectionStatus, + setActionLabel, + actionLabel, + errorLabel, + } = useKnockMSTeamsClient(); + + const { buildMSTeamsAuthUrl, disconnectFromMSTeams } = useMSTeamsAuth( + msTeamsBotId, + redirectUrl, + ); + + useEffect(() => { + const receiveMessage = (event: MessageEvent) => { + if (event.origin !== knock.host) { + return; + } + + try { + if (event.data === "authComplete") { + setConnectionStatus("connected"); + } + + if (event.data === "authFailed") { + setConnectionStatus("error"); + } + + if (onAuthenticationComplete) { + onAuthenticationComplete(event.data); + } + } catch (_error) { + setConnectionStatus("error"); + } + }; + + window.addEventListener("message", receiveMessage, false); + + // Cleanup the event listener when the component unmounts + return () => { + window.removeEventListener("message", receiveMessage); + }; + }, [knock.host, onAuthenticationComplete, setConnectionStatus]); + + const disconnectLabel = t("msTeamsDisconnect") || null; + const reconnectLabel = t("msTeamsReconnect") || null; + + // Loading states + if ( + connectionStatus === "connecting" || + connectionStatus === "disconnecting" + ) { + return ( +
+ + + {connectionStatus === "connecting" + ? t("msTeamsConnecting") + : t("msTeamsDisconnecting")} + +
+ ); + } + + // Error state + if (connectionStatus === "error") { + return ( + + ); + } + + // Disconnected state + if (connectionStatus === "disconnected") { + return ( + + ); + } + + // Connected state + return ( + + ); +}; diff --git a/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/index.ts b/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/index.ts new file mode 100644 index 00000000..66d63bda --- /dev/null +++ b/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/index.ts @@ -0,0 +1 @@ +export * from "./MSTeamsAuthButton"; diff --git a/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/styles.css b/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/styles.css new file mode 100644 index 00000000..b50e7f49 --- /dev/null +++ b/packages/react/src/modules/ms-teams/components/MSTeamsAuthButton/styles.css @@ -0,0 +1,63 @@ +:root { + --rtk-connected-color: rgba(51, 163, 102, 1); + --rtk-disconnect-border-color: rgba(230, 71, 51, 1); + --rtk-disconnect-background-color: rgba(255, 245, 245, 1); + --rtk-error-red: rgba(205, 123, 46, 1); +} + +.rtk-connect__button { + background-color: var(--rtk-color-white); + border: 1px solid var(--rtk-color-gray-200); + border-radius: var(--rtk-button-border-radius); + box-sizing: border-box; + color: var(--rtk-color-black); + cursor: pointer; + display: inline-flex; + font-family: var(--rtk-font-family-sanserif); + font-size: var(--rtk-font-size-sm); + font-weight: var(--rtk-font-weight-normal); + gap: var(--rtk-spacing-2); + padding: var(--rtk-spacing-1) var(--rtk-spacing-2); + text-decoration: none; + text-overflow: ellipsis; + text-wrap: nowrap; + transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; +} + +.rtk-connect__button--connected { + border-color: var(--rtk-connected-color); + color: var(--rtk-connected-color); + width: 120px; +} + +.rtk-connect__button--error { + border-color: var(--rtk-error-red); + color: var(--rtk-error-red); +} + +.rtk-connect__button--loading { + border-color: var(--rtk-color-gray-100); + color: var(--rtk-color-gray-400); + pointer-events: none; +} + +.rtk-connect__button--disconnected:hover { + background-color: var(--rtk-button-hover-color); +} + +.rtk-connect__button--connected:hover, +.rtk-connect__button__text--connected:hover { + background-color: var(--rtk-disconnect-background-color); + border-color: var(--rtk-disconnect-border-color); + color: var(--rtk-disconnect-border-color); +} + +.rtk-connect__button--error:hover, +.rtk-connect__button__text--error:hover { + border-color: var(--rtk-color-black); + color: var(--rtk-color-black); +} + +.rtk-connect__button:active { + transform: translate(1px, 1px); +} diff --git a/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/MSTeamsAuthContainer.tsx b/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/MSTeamsAuthContainer.tsx new file mode 100644 index 00000000..a37ae656 --- /dev/null +++ b/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/MSTeamsAuthContainer.tsx @@ -0,0 +1,30 @@ +import { useTranslations } from "@knocklabs/react-core"; +import { FunctionComponent } from "react"; + +import "../../theme.css"; +import { MSTeamsIcon } from "../MSTeamsIcon"; + +import "./styles.css"; + +export interface MSTeamsAuthContainerProps { + actionButton: React.ReactElement; +} + +export const MSTeamsAuthContainer: FunctionComponent< + MSTeamsAuthContainerProps +> = ({ actionButton }) => { + const { t } = useTranslations(); + + return ( +
+
+ +
{actionButton}
+
+
Microsoft Teams
+
+ {t("msTeamsConnectContainerDescription")} +
+
+ ); +}; diff --git a/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/index.ts b/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/index.ts new file mode 100644 index 00000000..25362934 --- /dev/null +++ b/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/index.ts @@ -0,0 +1 @@ +export * from "./MSTeamsAuthContainer"; diff --git a/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/styles.css b/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/styles.css new file mode 100644 index 00000000..d987be5c --- /dev/null +++ b/packages/react/src/modules/ms-teams/components/MSTeamsAuthContainer/styles.css @@ -0,0 +1,28 @@ +.rtk-auth { + background: var(--rtk-color-white); + border: 1px solid var(--rtk-color-gray-100); + border-radius: var(--rtk-border-radius-lg); + font-family: var(--rtk-font-family-sanserif); + font-size: var(--rtk-font-size-sm); + font-weight: var(--rtk-font-weight-normal); + padding: var(--rtk-spacing-5); + } + +.rtk-auth__header { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.rtk-auth__title { + color: rgba(26, 31, 54, 1); + font-size: var(--rtk-font-size-md); + line-height: var(--rtk-spacing-5); + margin-top: var(--rtk-spacing-4); +} + +.rtk-auth__description { + color: rgba(81, 86, 105, 1); + font-size: var(--rtk-font-size-sm); + line-height: var(--rtk-spacing-5); +} diff --git a/packages/react/src/modules/ms-teams/components/MSTeamsIcon/MSTeamsIcon.tsx b/packages/react/src/modules/ms-teams/components/MSTeamsIcon/MSTeamsIcon.tsx new file mode 100644 index 00000000..1f288b92 --- /dev/null +++ b/packages/react/src/modules/ms-teams/components/MSTeamsIcon/MSTeamsIcon.tsx @@ -0,0 +1,84 @@ +import { FunctionComponent } from "react"; + +export interface MSTeamsIconProps { + height: string; + width: string; +} + +export const MSTeamsIcon: FunctionComponent = ({ + height, + width, +}) => { + return ( + // Source: https://commons.wikimedia.org/wiki/File:Microsoft_Office_Teams_(2018%E2%80%93present).svg + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/react/src/modules/ms-teams/components/MSTeamsIcon/index.ts b/packages/react/src/modules/ms-teams/components/MSTeamsIcon/index.ts new file mode 100644 index 00000000..395aff02 --- /dev/null +++ b/packages/react/src/modules/ms-teams/components/MSTeamsIcon/index.ts @@ -0,0 +1 @@ +export * from "./MSTeamsIcon"; diff --git a/packages/react/src/modules/ms-teams/index.ts b/packages/react/src/modules/ms-teams/index.ts new file mode 100644 index 00000000..f0167384 --- /dev/null +++ b/packages/react/src/modules/ms-teams/index.ts @@ -0,0 +1,2 @@ +export * from "./components/MSTeamsAuthButton"; +export * from "./components/MSTeamsAuthContainer"; diff --git a/packages/react/src/modules/ms-teams/theme.css b/packages/react/src/modules/ms-teams/theme.css new file mode 100644 index 00000000..9a5e0c3c --- /dev/null +++ b/packages/react/src/modules/ms-teams/theme.css @@ -0,0 +1,54 @@ +:root { + /* Font sizes */ + --rtk-font-size-xs: 0.75rem; + --rtk-font-size-sm: 0.875rem; + --rtk-font-size-md: 1rem; + --rtk-font-size-lg: 1.125rem; + --rtk-font-size-xl: 1.266rem; + --rtk-font-size-2xl: 1.5rem; + --rtk-font-size-3xl: 1.75rem; + + /* Spacing */ + --rtk-spacing-0: 0rem; + --rtk-spacing-1: 0.25rem; + --rtk-spacing-2: 0.5rem; + --rtk-spacing-3: 0.75rem; + --rtk-spacing-4: 1rem; + --rtk-spacing-5: 1.25rem; + --rtk-spacing-6: 1.5rem; + --rtk-spacing-7: 2rem; + + /* Font weights */ + --rtk-font-weight-normal: 400; + --rtk-font-weight-medium: 500; + --rtk-font-weight-semibold: 600; + --rtk-font-weight-bold: 700; + + /* Font family */ + --rtk-font-family-sanserif: Inter, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif; + + /* Border radius */ + --rtk-border-radius-sm: 2px; + --rtk-border-radius-md: 4px; + --rtk-border-radius-lg: 8px; + --rtk-button-border-radius: 6px; + + /* Colors */ + --rtk-color-white: #fff; + --rtk-color-white-a-75: rgba(255, 255, 255, 0.75); + --rtk-color-black: #000; + --rtk-color-gray-900: #1a1f36; + --rtk-color-gray-800: #3c4257; + --rtk-color-gray-700: #3c4257; + --rtk-color-gray-600: #515669; + --rtk-color-gray-500: #697386; + --rtk-color-gray-400: #9ea0aa; + --rtk-color-gray-300: #a5acb8; + --rtk-color-gray-200: #dddee1; + --rtk-color-gray-100: #e4e8ee; + --rtk-color-brand-500: #e95744; + --rtk-color-brand-700: #e4321b; + --rtk-color-brand-900: #891e10; + --rtk-button-hover-color: rgba(247, 247, 248, 1); +} diff --git a/packages/react/src/modules/slack/components/SlackAuthButton/SlackAuthButton.tsx b/packages/react/src/modules/slack/components/SlackAuthButton/SlackAuthButton.tsx index d036e006..29f2efad 100644 --- a/packages/react/src/modules/slack/components/SlackAuthButton/SlackAuthButton.tsx +++ b/packages/react/src/modules/slack/components/SlackAuthButton/SlackAuthButton.tsx @@ -7,6 +7,7 @@ import { import { FunctionComponent } from "react"; import { useEffect } from "react"; +import { openPopupWindow } from "../../../core/utils"; import "../../theme.css"; import { SlackIcon } from "../SlackIcon"; @@ -19,29 +20,6 @@ export interface SlackAuthButtonProps { additionalScopes?: string[]; } -const openSlackOauthPopup = (url: string) => { - const width = 600; - const height = 800; - // Calculate the position to center the window - const screenLeft = window.screenLeft ?? window.screenX; - const screenTop = window.screenTop ?? window.screenY; - - const innerWidth = - window.innerWidth ?? document.documentElement.clientWidth ?? screen.width; - const innerHeight = - window.innerHeight ?? - document.documentElement.clientHeight ?? - screen.height; - - const left = innerWidth / 2 - width / 2 + screenLeft; - const top = innerHeight / 2 - height / 2 + screenTop; - - // Window features - const features = `width=${width},height=${height},top=${top},left=${left}`; - - window.open(url, "Slack OAuth", features); -}; - export const SlackAuthButton: FunctionComponent = ({ slackClientId, redirectUrl, @@ -120,7 +98,7 @@ export const SlackAuthButton: FunctionComponent = ({ if (connectionStatus === "error") { return ( ); @@ -121,12 +121,12 @@ export const MSTeamsAuthButton: FunctionComponent = ({ // Connected state return (