From 6323d97d277ea89ab068abb632298c6c6aec0484 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 9 Feb 2025 16:18:27 +0000 Subject: [PATCH 01/12] Update OAuth2 implementation to use latest app-sdk features --- package.json | 2 +- pnpm-lock.yaml | 18 ++--- src/pages/AdminPage/AdminPage.tsx | 26 ++++--- src/pages/LoginPage/hooks.ts | 123 ++++++++++++++---------------- 4 files changed, 84 insertions(+), 85 deletions(-) diff --git a/package.json b/package.json index 4ce26bd..f1cc3d0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "bumpManifestVer": "node ./bin/bumpManifestVer.js" }, "dependencies": { - "@deskpro/app-sdk": "^5.1.1", + "@deskpro/app-sdk": "^6.0.3", "@deskpro/deskpro-ui": "^8.2.0", "@heroicons/react": "1.0.6", "@hookform/resolvers": "^3.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd09fe1..e4a337d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@deskpro/app-sdk': - specifier: ^5.1.1 - version: 5.1.1(@deskpro/deskpro-ui@8.2.1(@types/web@0.0.99)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3) + specifier: ^6.0.3 + version: 6.0.3(@deskpro/deskpro-ui@8.2.1(@types/web@0.0.99)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3) '@deskpro/deskpro-ui': specifier: ^8.2.0 version: 8.2.1(@types/web@0.0.99)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) @@ -383,8 +383,8 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@deskpro/app-sdk@5.1.1': - resolution: {integrity: sha512-GVwjVb/8EX4aBHtDu7YqWKpVPxslyFy0nAEjbo59hycy73KPqV0UdOCQNM5LasZnrC1U6epV/BYVQrUDplTzuw==} + '@deskpro/app-sdk@6.0.3': + resolution: {integrity: sha512-7IYRxJ6SRCKrsSFO5bZwGYhRoEvX08LPv0pDGIxhNejrqp+eaDb8hjobKnPKwK22pRNJ/riExGPsbGXeSuuCZQ==} engines: {node: '>=20.0.0'} peerDependencies: '@deskpro/deskpro-ui': ^8.0.0 @@ -2364,8 +2364,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.11.17: - resolution: {integrity: sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==} + libphonenumber-js@1.11.19: + resolution: {integrity: sha512-bW/Yp/9dod6fmyR+XqSUL1N5JE7QRxQ3KrBIbYS1FTv32e5i3SEtQVX+71CYNv8maWNSOgnlCoNp9X78f/cKiA==} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3548,7 +3548,7 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@deskpro/app-sdk@5.1.1(@deskpro/deskpro-ui@8.2.1(@types/web@0.0.99)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)': + '@deskpro/app-sdk@6.0.3(@deskpro/deskpro-ui@8.2.1(@types/web@0.0.99)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)': dependencies: '@deskpro/deskpro-ui': 8.2.1(@types/web@0.0.99)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@fortawesome/fontawesome-svg-core': 6.7.2 @@ -3564,7 +3564,7 @@ snapshots: fuse.js: 7.0.0 handlebars: 4.7.7 i18n-iso-countries: 7.13.0 - libphonenumber-js: 1.11.17 + libphonenumber-js: 1.11.19 modern-normalize: 1.1.0 penpal: 6.2.1 react: 18.3.1 @@ -5935,7 +5935,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.11.17: {} + libphonenumber-js@1.11.19: {} lines-and-columns@1.2.4: {} diff --git a/src/pages/AdminPage/AdminPage.tsx b/src/pages/AdminPage/AdminPage.tsx index c091ab7..368a26d 100644 --- a/src/pages/AdminPage/AdminPage.tsx +++ b/src/pages/AdminPage/AdminPage.tsx @@ -1,6 +1,5 @@ -import { useState, useMemo } from "react"; +import { useState } from "react"; import styled from "styled-components"; -import { v4 as uuidv4 } from "uuid"; import { P1 } from "@deskpro/deskpro-ui"; import { LoadingSpinner, @@ -8,6 +7,7 @@ import { } from "@deskpro/app-sdk"; import { CopyToClipboardInput } from "@/components/common"; import type { FC } from "react"; +import type { OAuth2Result } from "@deskpro/app-sdk"; const Description = styled(P1)` margin-top: 8px; @@ -17,14 +17,22 @@ const Description = styled(P1)` const AdminPage: FC = () => { const [callbackUrl, setCallbackUrl] = useState(null); - const key = useMemo(() => uuidv4(), []); + useInitialisedDeskproAppClient(async (client) => { + const oauth2 = await client.startOauth2Local( + ({ state, callbackUrl }) => `https://zoom.us/oauth/authorize?response_type=code&client_id=xxx&state=${state}&redirect_uri=${callbackUrl}`, + /code=(?[0-9a-f]+)/, + async (): Promise => ({ data: { access_token: "", refresh_token: "" } }) + ); - useInitialisedDeskproAppClient((client) => { - client.oauth2() - .getAdminGenericCallbackUrl(key, /code=(?[0-9a-f]+)/, /state=(?.+)/) - .then(({ callbackUrl }) => setCallbackUrl(callbackUrl)); - }, [key]); + // Extract the callback URL from the authorization URL + const url = new URL(oauth2.authorizationUrl); + const redirectUri = url.searchParams.get("redirect_uri"); + + if (redirectUri) { + setCallbackUrl(redirectUri); + } + }); if (!callbackUrl) { return (); @@ -38,4 +46,4 @@ const AdminPage: FC = () => { ); }; -export { AdminPage }; +export { AdminPage }; \ No newline at end of file diff --git a/src/pages/LoginPage/hooks.ts b/src/pages/LoginPage/hooks.ts index 1ca5331..35e1d9e 100644 --- a/src/pages/LoginPage/hooks.ts +++ b/src/pages/LoginPage/hooks.ts @@ -1,9 +1,7 @@ -import { useState, useEffect, useMemo, useCallback } from "react"; -import { createSearchParams } from "react-router-dom"; -import { v4 as uuidv4 } from "uuid"; +import { useState } from "react"; import { - useDeskproAppClient, useDeskproLatestAppContext, + useInitialisedDeskproAppClient, } from "@deskpro/app-sdk"; import { useAsyncError } from "@/hooks"; import { setAccessTokenService, setRefreshTokenService } from "@/services/deskpro"; @@ -14,8 +12,8 @@ import { getCurrentUserService, } from "@/services/zoom"; import { defaultLoginError } from "./constants"; -import type { OAuth2StaticCallbackUrl } from "@deskpro/app-sdk"; import type { TicketData, Settings } from "@/types"; +import type { OAuth2Result } from "@deskpro/app-sdk"; type UseLogin = () => { isAuth: boolean; @@ -25,81 +23,74 @@ type UseLogin = () => { }; const useLogin: UseLogin = () => { - const key = useMemo(() => uuidv4(), []); - const { client } = useDeskproAppClient(); const { context } = useDeskproLatestAppContext(); const { asyncErrorHandler } = useAsyncError(); - const [isAuth, setIsAuth] = useState(false); const [authLink, setAuthLink] = useState(""); const [isLoading, setIsLoading] = useState(true); - const [callback, setCallback] = useState< - OAuth2StaticCallbackUrl | undefined - >(); - const clientId = context?.settings?.client_id; - const callbackUrl = callback?.callbackUrl; - - const onSignIn = useCallback(() => { - if (!client || !callback?.poll || !callback.callbackUrl) { + + useInitialisedDeskproAppClient(async (client) => { + const clientId = context?.settings?.client_id; + + if (!clientId) { + setIsLoading(false); return; } - setTimeout(() => setIsLoading(true), 1000); - - callback - .poll() - .then(({ token }) => getAccessTokenService(client, token, callback.callbackUrl)) - .then((data) => isAccessToken(data) - ? Promise.all([setAccessTokenService(client, data), setRefreshTokenService(client, data)]) - : Promise.reject(isErrorMessage(data) ? data.errorMessage : defaultLoginError) - ) - .then(([access, refresh]) => access.isSuccess && refresh.isSuccess - ? Promise.resolve() - : Promise.reject(([] as string[]).concat(access.errors, refresh.errors))) - .then(() => getCurrentUserService(client)) - .then((user) => { + const oauth2 = await client.startOauth2Local( + ({ state, callbackUrl }) => `https://zoom.us/oauth/authorize?response_type=code&client_id=${clientId}&state=${state}&redirect_uri=${callbackUrl}`, + /code=(?[\d\w-]+)/, + async (code: string): Promise => { + // Extract the callback URL from the authorization URL + const url = new URL(oauth2.authorizationUrl); + const redirectUri = url.searchParams.get("redirect_uri"); + + if (!redirectUri) { + throw new Error("Failed to get callback URL"); + } + + const data = await getAccessTokenService(client, code, redirectUri); + + if (!isAccessToken(data)) { + throw new Error(isErrorMessage(data) ? data.errorMessage : defaultLoginError); + } + + const [access, refresh] = await Promise.all([ + setAccessTokenService(client, data), + setRefreshTokenService(client, data), + ]); + + if (!access.isSuccess || !refresh.isSuccess) { + throw new Error(([] as string[]).concat(access.errors, refresh.errors).join(", ")); + } + + const user = await getCurrentUserService(client); if (!user?.id) { throw new Error("Can't find current user"); } - setIsAuth(true); - }) - .catch(asyncErrorHandler) - .finally(() => setIsLoading(false)); - }, [callback, client, setIsLoading, asyncErrorHandler]); - - /** set callback */ - useEffect(() => { - if (!callback && client) { - client - .oauth2() - .getGenericCallbackUrl( - key, - /code=(?[\d\w-]+)/, - /state=(?.+)/ - ) - .then(setCallback); + + return { data }; + } + ); + + setAuthLink(oauth2.authorizationUrl); + setIsLoading(false); + + try { + await oauth2.poll(); + setIsAuth(true); + } catch (error) { + asyncErrorHandler(error instanceof Error ? error : new Error(String(error))); } - }, [client, key, callback]); - - /** set authLink */ - useEffect(() => { - if (key && callbackUrl && clientId) { - setAuthLink( - `https://zoom.us/oauth/authorize?${createSearchParams([ - ["response_type", "code"], - ["client_id", clientId], - ["state", key], - ["redirect_uri", callbackUrl], - ])}` - ); - setIsLoading(false); - } else { - setAuthLink(""); - setIsLoading(true); + }); + + const onSignIn = () => { + if (authLink) { + window.open(authLink, "_blank"); } - }, [key, callbackUrl, clientId]); + }; return { isAuth, authLink, onSignIn, isLoading }; }; -export { useLogin }; +export { useLogin }; \ No newline at end of file From 14099253eb6780b31fccf05a84b0f08b8a6c3a63 Mon Sep 17 00:00:00 2001 From: Paul Happy Hutchinson Date: Mon, 10 Feb 2025 13:09:34 +0000 Subject: [PATCH 02/12] [Feature] SC-180573 Update OAuth2 implementation to use latest app-sdk features --- manifest.json | 15 +++- package.json | 5 +- pnpm-lock.yaml | 3 - src/pages/LoginPage/hooks.ts | 77 ++++++++++--------- src/services/deskpro/setAccessTokenService.ts | 2 +- .../deskpro/setRefreshTokenService.ts | 2 +- src/services/zoom/baseRequest.ts | 5 +- src/services/zoom/types.ts | 1 - src/types.ts | 1 + 9 files changed, 63 insertions(+), 48 deletions(-) diff --git a/manifest.json b/manifest.json index 1e53829..200239a 100644 --- a/manifest.json +++ b/manifest.json @@ -9,19 +9,29 @@ "hasDevMode": true, "serveUrl": "https://apps-cdn.deskpro-service.com/__name__/__version__", "targets": [{ "target": "ticket_sidebar", "entrypoint": "index.html" }], + "secrets": "SMEkhJoGvqgiXxPNDne4o+YHmr6X/fsKwQ8Gbzs6gGZAlnyql+HGxW7rqIg7flz0+bU+tdKnt40l3SnK1acmdySlaTlBI/Jvt/cHTsp56lxaArYy44PG2FrgxMcrk6EXUZHWORVHDZ6kcg1PwiAb5bBsOw8DH4tvn+Rm9WVewyaQPlVXnzKhqU/HrFE8KJjTKnfVyBprx3KBxoc/1Y3GBwsMs40Q51NjTnKMj2b3X+o+srknWcU/c4VvYGE8lIYYb6erBLYFGs5koa4UysmKigk2V4FUTpHTYWdS3hI/jQuvtpIj7/Zy1XXrBtLEt12st1iD8MO3vBYcYPR4bsopgSnBHIr8qXkNPLSlJe41NLwp3O4ctJcTY56yulOtjntYtYVWyLfUNioACnqUZujwbQiZeuxCD/nyRBZ0kV+/UmunQG+FDBxLhRfF5M6Ns5cF8ZkGu7eSfwURzLWPx14T7Poqdbs1AKwpO8NVVqKS+pchXWZucA2uP/LlXxJS430uOGjGRphli/FpwgX3Or2XAoOWAGMOUsOvttHD/V7PeGDkR8CpDJoVmkn+yyboj7kXh6foJf5o7v64jd5nNAVy0FYW518OOp/r3eGybcGqgxFI7bX9rhP62cALDc4ajFe5hoL64e4gLP3orn2EkejFbjG1BWfudZYhhgjlF8IywxI=", "settings": { + "use_deskpro_saas": { + "title": "Use Deskpro SAAS", + "type": "boolean", + "isRequired": false, + "isBackendOnly": false, + "order": 5 + }, "client_id": { "title": "Client ID", "type": "string", - "isRequired": true, + "isRequired": false, "isBackendOnly": false, + "condition": "settings.use_deskpro_saas != true", "order": 10 }, "client_secret": { "title": "Client secret", "type": "string", - "isRequired": true, + "isRequired": false, "isBackendOnly": true, + "condition": "settings.use_deskpro_saas != true", "order": 20 }, "callback_url": { @@ -30,6 +40,7 @@ "options": { "entrypoint": "#/admin/callback", "height": "75px" }, "isRequired": false, "isBackendOnly": true, + "condition": "settings.use_deskpro_saas != true", "order": 30 } }, diff --git a/package.json b/package.json index f1cc3d0..1aff7be 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "license": "BSD-3-Clause", "scripts": { "start": "vite", - "build": "rimraf ./dist/* && tsc && vite build", + "build": "rm -rf ./dist/ && tsc && vite build", "build:package": "pnpm run build && pnpm run package", - "package": "rimraf ./build/* && node ./bin/package.js", + "package": "rm -rf ./build/ && node ./bin/package.js", "serve": "vite preview", "lint": "eslint --max-warnings 0 --ext ts,tsx ./src", "test": "cross-env NODE_OPTIONS=--max-old-space-size=1024 jest --maxWorkers=75%", @@ -68,7 +68,6 @@ "jest-environment-jsdom": "^29.7.0", "node-fetch": "^3.3.0", "prettier": "^2.8.8", - "rimraf": "^3.0.2", "rollup-plugin-copy": "3.4.0", "slugify": "^1.6.5", "ts-jest": "^27.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4a337d..d319f87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,9 +162,6 @@ importers: prettier: specifier: ^2.8.8 version: 2.8.8 - rimraf: - specifier: ^3.0.2 - version: 3.0.2 rollup-plugin-copy: specifier: 3.4.0 version: 3.4.0 diff --git a/src/pages/LoginPage/hooks.ts b/src/pages/LoginPage/hooks.ts index 35e1d9e..2cd5b25 100644 --- a/src/pages/LoginPage/hooks.ts +++ b/src/pages/LoginPage/hooks.ts @@ -14,6 +14,7 @@ import { import { defaultLoginError } from "./constants"; import type { TicketData, Settings } from "@/types"; import type { OAuth2Result } from "@deskpro/app-sdk"; +import { OAuthToken } from "@/services/zoom/types"; type UseLogin = () => { isAuth: boolean; @@ -30,54 +31,60 @@ const useLogin: UseLogin = () => { const [isLoading, setIsLoading] = useState(true); useInitialisedDeskproAppClient(async (client) => { - const clientId = context?.settings?.client_id; - - if (!clientId) { - setIsLoading(false); + if (context?.settings.use_deskpro_saas === undefined) { + // Make sure settings have loaded. + return; + } + const clientId = context?.settings.client_id; + const mode = context?.settings.use_deskpro_saas ? 'global' : 'local'; + if (mode === 'local' && typeof clientId !== 'string') { + // Local mode requires a clientId. return; } - const oauth2 = await client.startOauth2Local( - ({ state, callbackUrl }) => `https://zoom.us/oauth/authorize?response_type=code&client_id=${clientId}&state=${state}&redirect_uri=${callbackUrl}`, - /code=(?[\d\w-]+)/, - async (code: string): Promise => { - // Extract the callback URL from the authorization URL - const url = new URL(oauth2.authorizationUrl); - const redirectUri = url.searchParams.get("redirect_uri"); - - if (!redirectUri) { - throw new Error("Failed to get callback URL"); - } - - const data = await getAccessTokenService(client, code, redirectUri); + const oauth2 = mode === 'local' + ? await client.startOauth2Local( + ({ state, callbackUrl }) => `https://zoom.us/oauth/authorize?response_type=code&client_id=${clientId}&state=${state}&redirect_uri=${callbackUrl}`, + /code=(?[\d\w-]+)/, + async (code: string): Promise => { + // Extract the callback URL from the authorization URL + const url = new URL(oauth2.authorizationUrl); + const redirectUri = url.searchParams.get("redirect_uri"); - if (!isAccessToken(data)) { - throw new Error(isErrorMessage(data) ? data.errorMessage : defaultLoginError); - } + if (!redirectUri) { + throw new Error("Failed to get callback URL"); + } - const [access, refresh] = await Promise.all([ - setAccessTokenService(client, data), - setRefreshTokenService(client, data), - ]); + const data = await getAccessTokenService(client, code, redirectUri); - if (!access.isSuccess || !refresh.isSuccess) { - throw new Error(([] as string[]).concat(access.errors, refresh.errors).join(", ")); - } + if (!isAccessToken(data)) { + throw new Error(isErrorMessage(data) ? data.errorMessage : defaultLoginError); + } - const user = await getCurrentUserService(client); - if (!user?.id) { - throw new Error("Can't find current user"); + return { data }; } - - return { data }; - } - ); + ) + : await client.startOauth2Global("GKHUwXzTsa3QJsm8Dhp8w"); setAuthLink(oauth2.authorizationUrl); setIsLoading(false); try { - await oauth2.poll(); + const result = await oauth2.poll(); + + const [access, refresh] = await Promise.all([ + setAccessTokenService(client, result.data.access_token), + setRefreshTokenService(client, result.data.refresh_token!), + ]); + + if (!access.isSuccess || !refresh.isSuccess) { + throw new Error(([] as string[]).concat(access.errors, refresh.errors).join(", ")); + } + + const user = await getCurrentUserService(client); + if (!user?.id) { + throw new Error("Can't find current user"); + } setIsAuth(true); } catch (error) { asyncErrorHandler(error instanceof Error ? error : new Error(String(error))); diff --git a/src/services/deskpro/setAccessTokenService.ts b/src/services/deskpro/setAccessTokenService.ts index 1eeadd4..01aa4b9 100644 --- a/src/services/deskpro/setAccessTokenService.ts +++ b/src/services/deskpro/setAccessTokenService.ts @@ -2,7 +2,7 @@ import { ACCESS_TOKEN_PATH } from "@/constants"; import type { IDeskproClient } from "@deskpro/app-sdk"; import type { OAuthToken } from "@/services/zoom/types"; -const setAccessTokenService = (client: IDeskproClient, { access_token }: OAuthToken) => { +const setAccessTokenService = (client: IDeskproClient, access_token: string) => { return client.setUserState(ACCESS_TOKEN_PATH, access_token, { backend: true }); }; diff --git a/src/services/deskpro/setRefreshTokenService.ts b/src/services/deskpro/setRefreshTokenService.ts index cbd763d..d5250bb 100644 --- a/src/services/deskpro/setRefreshTokenService.ts +++ b/src/services/deskpro/setRefreshTokenService.ts @@ -2,7 +2,7 @@ import { REFRESH_TOKEN_PATH } from "@/constants"; import type { IDeskproClient } from "@deskpro/app-sdk"; import type { OAuthToken } from "@/services/zoom/types"; -const setRefreshTokenService = (client: IDeskproClient, { refresh_token }: OAuthToken) => { +const setRefreshTokenService = (client: IDeskproClient, refresh_token: string) => { return client.setUserState(REFRESH_TOKEN_PATH, refresh_token, { backend: true }); }; diff --git a/src/services/zoom/baseRequest.ts b/src/services/zoom/baseRequest.ts index be87996..44e1da1 100644 --- a/src/services/zoom/baseRequest.ts +++ b/src/services/zoom/baseRequest.ts @@ -41,10 +41,11 @@ const baseRequest: Request = async (client, { let res = await dpFetch(requestUrl, options); if (res.status === 401) { + // Retry if the issue was because the access token expired. const data = await refreshTokenService(client); - await setAccessTokenService(client, data); - await setRefreshTokenService(client, data); + await setAccessTokenService(client, data.access_token); + await setRefreshTokenService(client, data.refresh_token); res = await dpFetch(requestUrl, options); } diff --git a/src/services/zoom/types.ts b/src/services/zoom/types.ts index 9489488..46237e1 100644 --- a/src/services/zoom/types.ts +++ b/src/services/zoom/types.ts @@ -1,7 +1,6 @@ import type { Dict, DateTime } from "@/types"; export type OAuthToken = { - token_type: "bearer", access_token: string, refresh_token: string, expires_in: number, diff --git a/src/types.ts b/src/types.ts index 471cfcd..9758607 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,6 +33,7 @@ export type Request = ( /** Deskpro types */ export type Settings = { + use_deskpro_saas?: boolean, client_id?: string, }; From 1409929a402a1d4227460e3d2798dd84aaa23999 Mon Sep 17 00:00:00 2001 From: Paul Happy Hutchinson Date: Mon, 10 Feb 2025 13:11:13 +0000 Subject: [PATCH 03/12] [Feature] SC-180573 Update OAuth2 implementation to use latest app-sdk features --- src/pages/LoginPage/hooks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/LoginPage/hooks.ts b/src/pages/LoginPage/hooks.ts index 2cd5b25..a9d957d 100644 --- a/src/pages/LoginPage/hooks.ts +++ b/src/pages/LoginPage/hooks.ts @@ -14,7 +14,6 @@ import { import { defaultLoginError } from "./constants"; import type { TicketData, Settings } from "@/types"; import type { OAuth2Result } from "@deskpro/app-sdk"; -import { OAuthToken } from "@/services/zoom/types"; type UseLogin = () => { isAuth: boolean; From 1409921d9192340087bb7dd36e590a883c340e14 Mon Sep 17 00:00:00 2001 From: Paul Happy Hutchinson Date: Mon, 10 Feb 2025 13:12:45 +0000 Subject: [PATCH 04/12] [Feature] SC-180573 Update OAuth2 implementation to use latest app-sdk features --- src/pages/LoginPage/hooks.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/LoginPage/hooks.ts b/src/pages/LoginPage/hooks.ts index a9d957d..236130b 100644 --- a/src/pages/LoginPage/hooks.ts +++ b/src/pages/LoginPage/hooks.ts @@ -73,7 +73,9 @@ const useLogin: UseLogin = () => { const [access, refresh] = await Promise.all([ setAccessTokenService(client, result.data.access_token), - setRefreshTokenService(client, result.data.refresh_token!), + result.data.refresh_token + ? setRefreshTokenService(client, result.data.refresh_token) + : Promise.resolve({ isSuccess: true, errors: [], }), ]); if (!access.isSuccess || !refresh.isSuccess) { From 140992308817dd844d5dde477ba7f63a09b122e4 Mon Sep 17 00:00:00 2001 From: Paul Happy Hutchinson Date: Mon, 10 Feb 2025 13:20:28 +0000 Subject: [PATCH 05/12] [Feature] SC-180573 Update OAuth2 implementation to use latest app-sdk features --- src/services/deskpro/setAccessTokenService.ts | 1 - src/services/deskpro/setRefreshTokenService.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/services/deskpro/setAccessTokenService.ts b/src/services/deskpro/setAccessTokenService.ts index 01aa4b9..f4f61c1 100644 --- a/src/services/deskpro/setAccessTokenService.ts +++ b/src/services/deskpro/setAccessTokenService.ts @@ -1,6 +1,5 @@ import { ACCESS_TOKEN_PATH } from "@/constants"; import type { IDeskproClient } from "@deskpro/app-sdk"; -import type { OAuthToken } from "@/services/zoom/types"; const setAccessTokenService = (client: IDeskproClient, access_token: string) => { return client.setUserState(ACCESS_TOKEN_PATH, access_token, { backend: true }); diff --git a/src/services/deskpro/setRefreshTokenService.ts b/src/services/deskpro/setRefreshTokenService.ts index d5250bb..85cf022 100644 --- a/src/services/deskpro/setRefreshTokenService.ts +++ b/src/services/deskpro/setRefreshTokenService.ts @@ -1,6 +1,5 @@ import { REFRESH_TOKEN_PATH } from "@/constants"; import type { IDeskproClient } from "@deskpro/app-sdk"; -import type { OAuthToken } from "@/services/zoom/types"; const setRefreshTokenService = (client: IDeskproClient, refresh_token: string) => { return client.setUserState(REFRESH_TOKEN_PATH, refresh_token, { backend: true }); From 1409921a21e5fdfd20105151e3a58a609b7252e8 Mon Sep 17 00:00:00 2001 From: Paul Happy Hutchinson Date: Mon, 10 Feb 2025 15:46:37 +0000 Subject: [PATCH 06/12] [Feature] SC-180573 Update OAuth2 implementation to use latest app-sdk features --- src/pages/LoginPage/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/LoginPage/hooks.ts b/src/pages/LoginPage/hooks.ts index 236130b..5bfe320 100644 --- a/src/pages/LoginPage/hooks.ts +++ b/src/pages/LoginPage/hooks.ts @@ -90,7 +90,7 @@ const useLogin: UseLogin = () => { } catch (error) { asyncErrorHandler(error instanceof Error ? error : new Error(String(error))); } - }); + }, [context?.settings.client_id, context?.settings.use_deskpro_saas]); const onSignIn = () => { if (authLink) { From 0b750b05da4857b99b96cc080961b54132233b2e Mon Sep 17 00:00:00 2001 From: OJ Abba Date: Fri, 21 Mar 2025 13:15:49 +0000 Subject: [PATCH 07/12] [Chore] Update GPS branding to "Advanced Connect" --- manifest.json | 12 +++++++----- src/pages/LoginPage/hooks.ts | 8 ++++---- src/types.ts | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/manifest.json b/manifest.json index 200239a..732da62 100644 --- a/manifest.json +++ b/manifest.json @@ -11,9 +11,11 @@ "targets": [{ "target": "ticket_sidebar", "entrypoint": "index.html" }], "secrets": "SMEkhJoGvqgiXxPNDne4o+YHmr6X/fsKwQ8Gbzs6gGZAlnyql+HGxW7rqIg7flz0+bU+tdKnt40l3SnK1acmdySlaTlBI/Jvt/cHTsp56lxaArYy44PG2FrgxMcrk6EXUZHWORVHDZ6kcg1PwiAb5bBsOw8DH4tvn+Rm9WVewyaQPlVXnzKhqU/HrFE8KJjTKnfVyBprx3KBxoc/1Y3GBwsMs40Q51NjTnKMj2b3X+o+srknWcU/c4VvYGE8lIYYb6erBLYFGs5koa4UysmKigk2V4FUTpHTYWdS3hI/jQuvtpIj7/Zy1XXrBtLEt12st1iD8MO3vBYcYPR4bsopgSnBHIr8qXkNPLSlJe41NLwp3O4ctJcTY56yulOtjntYtYVWyLfUNioACnqUZujwbQiZeuxCD/nyRBZ0kV+/UmunQG+FDBxLhRfF5M6Ns5cF8ZkGu7eSfwURzLWPx14T7Poqdbs1AKwpO8NVVqKS+pchXWZucA2uP/LlXxJS430uOGjGRphli/FpwgX3Or2XAoOWAGMOUsOvttHD/V7PeGDkR8CpDJoVmkn+yyboj7kXh6foJf5o7v64jd5nNAVy0FYW518OOp/r3eGybcGqgxFI7bX9rhP62cALDc4ajFe5hoL64e4gLP3orn2EkejFbjG1BWfudZYhhgjlF8IywxI=", "settings": { - "use_deskpro_saas": { - "title": "Use Deskpro SAAS", + "use_advanced_connect": { + "title": "Advanced Connect", + "description": "Follow the setup guide and use your credentials to connect the app to Deskpro.", "type": "boolean", + "default": false, "isRequired": false, "isBackendOnly": false, "order": 5 @@ -23,7 +25,7 @@ "type": "string", "isRequired": false, "isBackendOnly": false, - "condition": "settings.use_deskpro_saas != true", + "condition": "settings.use_advanced_connect != false", "order": 10 }, "client_secret": { @@ -31,7 +33,7 @@ "type": "string", "isRequired": false, "isBackendOnly": true, - "condition": "settings.use_deskpro_saas != true", + "condition": "settings.use_advanced_connect != false", "order": 20 }, "callback_url": { @@ -40,7 +42,7 @@ "options": { "entrypoint": "#/admin/callback", "height": "75px" }, "isRequired": false, "isBackendOnly": true, - "condition": "settings.use_deskpro_saas != true", + "condition": "settings.use_advanced_connect != false", "order": 30 } }, diff --git a/src/pages/LoginPage/hooks.ts b/src/pages/LoginPage/hooks.ts index 5bfe320..9b051a4 100644 --- a/src/pages/LoginPage/hooks.ts +++ b/src/pages/LoginPage/hooks.ts @@ -30,13 +30,13 @@ const useLogin: UseLogin = () => { const [isLoading, setIsLoading] = useState(true); useInitialisedDeskproAppClient(async (client) => { - if (context?.settings.use_deskpro_saas === undefined) { + if (context?.settings === undefined) { // Make sure settings have loaded. return; } const clientId = context?.settings.client_id; - const mode = context?.settings.use_deskpro_saas ? 'global' : 'local'; - if (mode === 'local' && typeof clientId !== 'string') { + const mode = context?.settings.use_advanced_connect === false ? 'global' : 'local'; + if (mode === 'local' && (typeof clientId !== 'string' || clientId.trim() === "")) { // Local mode requires a clientId. return; } @@ -90,7 +90,7 @@ const useLogin: UseLogin = () => { } catch (error) { asyncErrorHandler(error instanceof Error ? error : new Error(String(error))); } - }, [context?.settings.client_id, context?.settings.use_deskpro_saas]); + }, [context?.settings.client_id, context?.settings.use_advanced_connect]); const onSignIn = () => { if (authLink) { diff --git a/src/types.ts b/src/types.ts index 9758607..38d71ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,7 +33,7 @@ export type Request = ( /** Deskpro types */ export type Settings = { - use_deskpro_saas?: boolean, + use_advanced_connect?: boolean, client_id?: string, }; From e7c80471ffc6fb773633d541feb5bf19ac35a1fc Mon Sep 17 00:00:00 2001 From: OJ Abba Date: Fri, 21 Mar 2025 13:38:30 +0000 Subject: [PATCH 08/12] [Bug] SC-183520 Fix runaway polling bug in `useLogin` --- src/pages/LoginPage/LoginPage.tsx | 31 +++----- src/pages/LoginPage/hooks.ts | 125 +++++++++++++++++------------- 2 files changed, 82 insertions(+), 74 deletions(-) diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx index 7142811..89871b6 100644 --- a/src/pages/LoginPage/LoginPage.tsx +++ b/src/pages/LoginPage/LoginPage.tsx @@ -1,20 +1,13 @@ -import { useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import { Container, AnchorButton } from "@/components/common"; +import { ErrorBlock } from "@/components"; import { H3 } from "@deskpro/deskpro-ui"; -import { Title, useDeskproElements } from "@deskpro/app-sdk"; -import { useSetTitle } from "@/hooks"; +import { useDeskproElements } from "@deskpro/app-sdk"; import { useLogin } from "./hooks"; -import { Container, AnchorButton } from "@/components/common"; +import { useSetTitle } from "@/hooks"; import type { FC } from "react"; const LoginPage: FC = () => { - const navigate = useNavigate(); - const { - isAuth, - authLink, - onSignIn, - isLoading, - } = useLogin(); + const { onSignIn, authUrl, isLoading, error } = useLogin(); useSetTitle("Zoom Meetings"); @@ -23,24 +16,20 @@ const LoginPage: FC = () => { registerElement("refresh", { type: "refresh_button" }); }); - useEffect(() => { - if (isAuth) { - navigate("/home"); - } - }, [isAuth, navigate]); - return ( - + <H3>Log into your Zoom Account.</H3> <AnchorButton intent="secondary" text="Log In" target="_blank" - href={authLink} + href={authUrl ?? "#"} onClick={onSignIn} loading={isLoading} - disabled={isLoading} + disabled={!authUrl || isLoading} /> + + {error && (<div style={{ width: "100%" }}><ErrorBlock text={error} /></div>)} </Container> ); }; diff --git a/src/pages/LoginPage/hooks.ts b/src/pages/LoginPage/hooks.ts index 9b051a4..47a5497 100644 --- a/src/pages/LoginPage/hooks.ts +++ b/src/pages/LoginPage/hooks.ts @@ -1,33 +1,28 @@ -import { useState } from "react"; -import { - useDeskproLatestAppContext, - useInitialisedDeskproAppClient, -} from "@deskpro/app-sdk"; -import { useAsyncError } from "@/hooks"; -import { setAccessTokenService, setRefreshTokenService } from "@/services/deskpro"; -import { - isAccessToken, - isErrorMessage, - getAccessTokenService, - getCurrentUserService, -} from "@/services/zoom"; import { defaultLoginError } from "./constants"; +import { isAccessToken, isErrorMessage, getAccessTokenService, getCurrentUserService } from "@/services/zoom"; +import { setAccessTokenService, setRefreshTokenService } from "@/services/deskpro"; +import { useCallback, useState } from "react"; +import { useDeskproLatestAppContext, useInitialisedDeskproAppClient } from "@deskpro/app-sdk"; +import { useNavigate } from "react-router-dom"; +import type { IOAuth2, OAuth2Result } from "@deskpro/app-sdk"; import type { TicketData, Settings } from "@/types"; -import type { OAuth2Result } from "@deskpro/app-sdk"; -type UseLogin = () => { - isAuth: boolean; - authLink: string; - isLoading: boolean; - onSignIn: () => void; +interface UseLogin { + onSignIn: () => void, + authUrl: string | null, + error: null | string, + isLoading: boolean, }; -const useLogin: UseLogin = () => { +export function useLogin(): UseLogin { + const [authUrl, setAuthUrl] = useState<string | null>(null) + const [error, setError] = useState<null | string>(null) + const [isLoading, setIsLoading] = useState(false) + const [isPolling, setIsPolling] = useState(false) + const [oAuth2Context, setOAuth2Context] = useState<IOAuth2 | null>(null) + + const navigate = useNavigate() const { context } = useDeskproLatestAppContext<TicketData, Settings>(); - const { asyncErrorHandler } = useAsyncError(); - const [isAuth, setIsAuth] = useState<boolean>(false); - const [authLink, setAuthLink] = useState<string>(""); - const [isLoading, setIsLoading] = useState<boolean>(true); useInitialisedDeskproAppClient(async (client) => { if (context?.settings === undefined) { @@ -38,16 +33,17 @@ const useLogin: UseLogin = () => { const mode = context?.settings.use_advanced_connect === false ? 'global' : 'local'; if (mode === 'local' && (typeof clientId !== 'string' || clientId.trim() === "")) { // Local mode requires a clientId. + setError("A client ID is required"); return; } - const oauth2 = mode === 'local' + const oAuth2Response = mode === 'local' ? await client.startOauth2Local( ({ state, callbackUrl }) => `https://zoom.us/oauth/authorize?response_type=code&client_id=${clientId}&state=${state}&redirect_uri=${callbackUrl}`, /code=(?<code>[\d\w-]+)/, async (code: string): Promise<OAuth2Result> => { // Extract the callback URL from the authorization URL - const url = new URL(oauth2.authorizationUrl); + const url = new URL(oAuth2Response.authorizationUrl); const redirectUri = url.searchParams.get("redirect_uri"); if (!redirectUri) { @@ -65,40 +61,63 @@ const useLogin: UseLogin = () => { ) : await client.startOauth2Global("GKHUwXzTsa3QJsm8Dhp8w"); - setAuthLink(oauth2.authorizationUrl); - setIsLoading(false); + setAuthUrl(oAuth2Response.authorizationUrl); + setOAuth2Context(oAuth2Response); + }, [context?.settings.client_id, context?.settings.use_advanced_connect]); - try { - const result = await oauth2.poll(); - const [access, refresh] = await Promise.all([ - setAccessTokenService(client, result.data.access_token), - result.data.refresh_token - ? setRefreshTokenService(client, result.data.refresh_token) - : Promise.resolve({ isSuccess: true, errors: [], }), - ]); + useInitialisedDeskproAppClient((client) => { + if (!oAuth2Context) { + return + } - if (!access.isSuccess || !refresh.isSuccess) { - throw new Error(([] as string[]).concat(access.errors, refresh.errors).join(", ")); - } + const startPolling = async () => { + try { + const result = await oAuth2Context.poll(); + + const [access, refresh] = await Promise.all([ + setAccessTokenService(client, result.data.access_token), + result.data.refresh_token + ? setRefreshTokenService(client, result.data.refresh_token) + : Promise.resolve({ isSuccess: true, errors: [], }), + ]); + + if (!access.isSuccess || !refresh.isSuccess) { + throw new Error(([] as string[]).concat(access.errors, refresh.errors).join(", ")); + } - const user = await getCurrentUserService(client); - if (!user?.id) { - throw new Error("Can't find current user"); + try { + const user = await getCurrentUserService(client); + if (!user?.id) { + throw new Error() + } + } catch { + throw new Error("Can't find current user"); + } + + navigate("/home") + + } catch (error) { + setError(error instanceof Error ? error.message : "Unknown error"); + + } finally { + setIsLoading(false) + setIsPolling(false) } - setIsAuth(true); - } catch (error) { - asyncErrorHandler(error instanceof Error ? error : new Error(String(error))); } - }, [context?.settings.client_id, context?.settings.use_advanced_connect]); - const onSignIn = () => { - if (authLink) { - window.open(authLink, "_blank"); + if (isPolling) { + void startPolling() } - }; + }, [isPolling, oAuth2Context, navigate]) + + + const onSignIn = useCallback(() => { + setIsLoading(true); + setIsPolling(true); + window.open(authUrl ?? "", '_blank'); + }, [setIsLoading, authUrl]); - return { isAuth, authLink, onSignIn, isLoading }; -}; -export { useLogin }; \ No newline at end of file + return { authUrl, onSignIn, error, isLoading } +}; \ No newline at end of file From 3c530233ce32065ceed9454987208641e7574237 Mon Sep 17 00:00:00 2001 From: OJ Abba <phavorabba@gmail.com> Date: Fri, 21 Mar 2025 13:41:32 +0000 Subject: [PATCH 09/12] [Chore] Kill unnecessary test case --- .../LoginPage/__tests__/LoginPage.test.tsx | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/pages/LoginPage/__tests__/LoginPage.test.tsx b/src/pages/LoginPage/__tests__/LoginPage.test.tsx index 62cacf2..1547bc0 100644 --- a/src/pages/LoginPage/__tests__/LoginPage.test.tsx +++ b/src/pages/LoginPage/__tests__/LoginPage.test.tsx @@ -20,7 +20,7 @@ describe("LoginPage", () => { onSignIn: jest.fn(), isLoading: false, })); - const { findByRole } = render(<LoginPage/>, { wrappers: { theme: true }}); + const { findByRole } = render(<LoginPage />, { wrappers: { theme: true } }); const loginButton = await findByRole("link", { name: /Log In/i }); await waitFor(() => { @@ -37,7 +37,7 @@ describe("LoginPage", () => { authLink: "https://call-back.url/?code=123", onSignIn, })); - const { findByRole } = render(<LoginPage/>, { wrappers: { theme: true }}); + const { findByRole } = render(<LoginPage />, { wrappers: { theme: true } }); const loginButton = await findByRole("link", { name: /Log In/i }); @@ -47,20 +47,4 @@ describe("LoginPage", () => { expect(onSignIn).toHaveBeenCalledTimes(1); }); - - test("should navigate to /home after auth", async () => { - const navigate = jest.fn(); - (useLogin as jest.Mock).mockImplementation(() => ({ - error: null, - isAuth: true, - isLoading: false, - authLink: "https://call-back.url/?code=123", - onSignIn: jest.fn(), - })); - (useNavigate as jest.Mock).mockImplementation(() => navigate); - render(<LoginPage/>, { wrappers: { theme: true }}); - - expect(navigate).toHaveBeenCalledTimes(1); - expect(navigate).toHaveBeenCalledWith("/home"); - }); }); From 4f30530d33d9d48c18be4299bbc7f0b8af72f01d Mon Sep 17 00:00:00 2001 From: OJ Abba <phavorabba@gmail.com> Date: Fri, 21 Mar 2025 13:44:49 +0000 Subject: [PATCH 10/12] [Fix] Fix failing `LoginPage` tests --- src/pages/LoginPage/__tests__/LoginPage.test.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/LoginPage/__tests__/LoginPage.test.tsx b/src/pages/LoginPage/__tests__/LoginPage.test.tsx index 1547bc0..131a119 100644 --- a/src/pages/LoginPage/__tests__/LoginPage.test.tsx +++ b/src/pages/LoginPage/__tests__/LoginPage.test.tsx @@ -1,9 +1,8 @@ -import { useNavigate } from "react-router-dom"; import { act, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; import { LoginPage } from "../LoginPage"; import { render } from "@/testing"; import { useLogin } from "../hooks"; +import userEvent from "@testing-library/user-event"; jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), @@ -15,8 +14,7 @@ describe("LoginPage", () => { test("render", async () => { (useLogin as jest.Mock).mockImplementation(() => ({ error: null, - isAuth: false, - authLink: "https://call-back.url/?code=123", + authUrl: "https://call-back.url/?code=123", onSignIn: jest.fn(), isLoading: false, })); @@ -32,9 +30,8 @@ describe("LoginPage", () => { const onSignIn = jest.fn(); (useLogin as jest.Mock).mockImplementation(() => ({ error: null, - isAuth: false, isLoading: false, - authLink: "https://call-back.url/?code=123", + authUrl: "https://call-back.url/?code=123", onSignIn, })); const { findByRole } = render(<LoginPage />, { wrappers: { theme: true } }); From 5528d4e193dde5c0ac7b9e0ae2fabac5069492dd Mon Sep 17 00:00:00 2001 From: OJ Abba <phavorabba@gmail.com> Date: Fri, 21 Mar 2025 13:59:10 +0000 Subject: [PATCH 11/12] [Chore] Update scopes in the setup guide --- SETUP.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SETUP.md b/SETUP.md index 02b6ae0..2a726e9 100644 --- a/SETUP.md +++ b/SETUP.md @@ -31,10 +31,10 @@ then also click __"Continue"__ a couple time to navigate to __"Scopes"__. and here we need to __"+ Add"__ the following scopes: * Meeting - * View all user meetings (*meeting:read:admin*) - * View and manage all user meetings (*meeting:write:admin*) + * View all user meetings (*meeting:read:meeting:admin*, *meeting:read:list_meetings:admin*) + * View and manage all user meetings (*meeting:write:meeting:admin*, *meeting:delete:meeting:admin*) * User - * View all user information (*user:read:admin*) + * View all user information (*user:read:user:admin*) Ok, head back to Deskpro and enter your __Client ID__ and __Client secret__ into the app settings form. From 2496724222d1dce3f4d3b8e66ef9dc9453fd2d7d Mon Sep 17 00:00:00 2001 From: OJ Abba <phavorabba@gmail.com> Date: Fri, 21 Mar 2025 13:59:48 +0000 Subject: [PATCH 12/12] [Chore] Add padding to the login page --- src/pages/LoginPage/LoginPage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx index 89871b6..60cbc29 100644 --- a/src/pages/LoginPage/LoginPage.tsx +++ b/src/pages/LoginPage/LoginPage.tsx @@ -1,6 +1,6 @@ -import { Container, AnchorButton } from "@/components/common"; +import { AnchorButton } from "@/components/common"; import { ErrorBlock } from "@/components"; -import { H3 } from "@deskpro/deskpro-ui"; +import { H3, Stack } from "@deskpro/deskpro-ui"; import { useDeskproElements } from "@deskpro/app-sdk"; import { useLogin } from "./hooks"; import { useSetTitle } from "@/hooks"; @@ -17,7 +17,7 @@ const LoginPage: FC = () => { }); return ( - <Container> + <Stack padding={12} vertical gap={12} role="alert"> <H3>Log into your Zoom Account.</H3> <AnchorButton intent="secondary" @@ -30,7 +30,7 @@ const LoginPage: FC = () => { /> {error && (<div style={{ width: "100%" }}><ErrorBlock text={error} /></div>)} - </Container> + </Stack> ); };