diff --git a/README.md b/README.md index 6e6c70c..0a20424 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Deskpro # Salesforce App - + View and manage associated Salesforce Contacts, Leads, Accounts and Opportunities ![Salesforce App - Deskpro](https://raw.githubusercontent.com/DeskproApps/salesforce/master/docs/assets/salesforce_screenshot_01.png) diff --git a/manifest.json b/manifest.json index 2dab775..841e389 100644 --- a/manifest.json +++ b/manifest.json @@ -8,29 +8,46 @@ "isSingleInstall": true, "hasDevMode": true, "serveUrl": "https://apps-cdn.deskpro-service.com/__name__/__version__", + "secrets": "NwvkQ+dBHVYVbJvqGxDmQ+EtdPKHnCNvVW1YTthncp2AdfXnZvCT4nU4V64BHK0Qr4tpZuktZksT1b0DzWycS6DfiuQXcXTXkcGr6zLLajBTUMd0jDExUXQ88kmlYnh20OzZFtSvf1KKIUTGZYo64E4YwKR/T1o4RwexzBkp0VcM8VCw/pNw0H/2bQgWyaruFZFmiKjz1Bc9zzRM21mbo5pR2s/hSr94070RYwUDHGSWDuCJi88weuM2b9T7X3kwWOS5gD/tdUK1IeZQ7OVoqpjmZsDnIJ3cp4enpF0gF19rs/eR15p1woNWStEqWZL9cxAM2QdigcFdtLvXqN15cnzG2Y4pkpbUJ+IUfBjlwaPh5ylzZGWoFpiIiRfd/MteGk6urcarSHK9U59XrJ/1aEY9643HaoQbWBJ12GeuUyRgnNAuPBsEs1iUDOAVgIlLHuG8FMmaZmSeY4SrHJEZN5BjAbbY3DjgyPFG2Zyvzp5cmo4F36dlZM6s/esKsiO0IdQdCt5rbXoKzOSwl134M1Aw8C3jC/rWuBhXMeMHgUWDbwuoV7Ut6ZJUg9GpoeZyq45VY98H4gjudK4ZvrVB4oB6DowHOTltEPKw8nK8hqVL5dBx3Dy1ptyzzWbeA44OBAeThsiHfsAtqKdZKefbMxYR80ZiqqlIECnqCvZEQdl8c9tPlpE9kEANjb9qhAYuJPX7vPNtDrX9pLWw0lSbEOQ2UE2EPfzzsX/4gwlmy0OhKSVntddHdtC0iHZAqe77BOs5fH+p9uJxJkz9NCLgUUhUv/OApLr5KY79blJN/sxUpj3rUbv+Xygghf0Lrm++WZL2kUVVobOWgE7eB9KTy0xHLQbgTVyreOGjn9OYaNonriwlwJ66Ylb8V9MdXlR41CbY04FpftymC5uNPkS1Oj7qw/oUEn5jsc8uNNXcs0mcDcnP2ch0sbz0c5NHlWQ8xMlT4JARL+6bh9R2JlCcnrFpuGpe9ym4ywbJakv9lGzMIJGDCt+5Nu1U9It+u6Idx4sO97NAM4Hov0MDgDBDtw+ZrM53onvwxPrIGwEDAlr17N+oIToyCIdRFMz40loLI++IgyA5qKgNxkBw29przslDGaVD9lxCO4IvG78DwW0Nk9F0mbrNS2fmNrHcOSf8JKpHS2jPyMscHtwq5ugUyk/C1TBvT4HI14KUt+tMhS8Fr9hWKujhPo+uovrE9b72ncG5Hn4vNSuwJVHbDly/Zz3i4XM0+JPqeuBlVDqubTk8DpEOC1wVp/M83kH9JR9H", "repository": { "type": "github", "url": "https://github.com/DeskproApps/salesforce" }, "targets": [ - { "target": "user_sidebar", "entrypoint": "#/user" }, - { "target": "organisation_sidebar", "entrypoint": "#/organization" } + { + "target": "user_sidebar", + "entrypoint": "#/user" + }, + { + "target": "organisation_sidebar", + "entrypoint": "#/organization" + } ], "settings": { + "use_deskpro_saas": { + "title": "One-Click Installation", + "type": "boolean", + "default": true, + "isRequired": false, + "isBackendOnly": false, + "order": 5 + }, "client_key": { "title": "Client Key", "description": "Client key can be obtained by following our setup guide", "type": "string", - "isRequired": true, + "isRequired": false, "isBackendOnly": false, + "condition": "settings.use_deskpro_saas != true", "order": 10 }, "client_secret": { "title": "Client Secret", "description": "Client secret can be obtained by following our setup guide", "type": "string", - "isRequired": true, + "isRequired": false, "isBackendOnly": true, + "condition": "settings.use_deskpro_saas != true", "order": 20 }, "salesforce_instance_url": { @@ -41,13 +58,28 @@ "isBackendOnly": false, "order": 30 }, + "callback_url": { + "title": "Callback URL", + "type": "app_embedded", + "options": { + "entrypoint": "#/admin/callback", + "height": "50px" + }, + "isRequired": false, + "isBackendOnly": true, + "condition": "settings.use_deskpro_saas != true", + "order": 40 + }, "global_access_token": { "title": "", "type": "app_embedded", - "options": { "entrypoint": "#/admin/global-sign-in" }, + "options": { + "entrypoint": "#/admin/global-sign-in", + "height": "120px" + }, "isRequired": true, "isBackendOnly": true, - "order": 40 + "order": 50 }, "mapping_contact": { "title": "Contact Field Mapping", @@ -60,7 +92,7 @@ }, "isRequired": false, "isBackendOnly": false, - "order": 50 + "order": 60 }, "mapping_lead": { "title": "Lead Field Mapping", @@ -73,7 +105,7 @@ }, "isRequired": false, "isBackendOnly": false, - "order": 60 + "order": 70 }, "mapping_account": { "title": "Account Field Mapping", @@ -86,7 +118,7 @@ }, "isRequired": false, "isBackendOnly": false, - "order": 70 + "order": 80 }, "mapping_note": { "title": "Note Field Mapping", @@ -99,7 +131,7 @@ }, "isRequired": false, "isBackendOnly": false, - "order": 80 + "order": 90 }, "mapping_opportunity": { "title": "Opportunity Field Mapping", @@ -112,7 +144,7 @@ }, "isRequired": false, "isBackendOnly": false, - "order": 90 + "order": 100 }, "mapping_task": { "title": "Task Field Mapping", @@ -125,7 +157,7 @@ }, "isRequired": false, "isBackendOnly": false, - "order": 100 + "order": 110 }, "mapping_event": { "title": "Event Field Mapping", @@ -138,16 +170,22 @@ }, "isRequired": false, "isBackendOnly": false, - "order": 110 + "order": 120 } }, "proxy": { "whitelist": [ { "url": "https://(.*).salesforce.com/services/.*", - "methods": ["GET", "POST", "PUT", "DELETE", "PATCH"], + "methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH" + ], "timeout": 30 } ] } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 1c0da22..0027600 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "@adobe/css-tools": "4.3.2", "@babel/plugin-transform-react-jsx": "^7.25.9", - "@deskpro/app-sdk": "^5.1.1", + "@deskpro/app-sdk": "^6.0.3", "@deskpro/deskpro-ui": "^8.2.1", "@hookform/resolvers": "^2.9.11", "@swc/core": "^1.10.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68beee1..3361a9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^7.25.9 version: 7.25.9(@babel/core@7.26.7) '@deskpro/app-sdk': - specifier: ^5.1.1 - version: 5.1.1(@deskpro/deskpro-ui@8.2.1(@types/web@0.0.86)(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.86)(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.1 version: 8.2.1(@types/web@0.0.86)(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)) @@ -424,8 +424,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 @@ -3852,7 +3852,7 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@deskpro/app-sdk@5.1.1(@deskpro/deskpro-ui@8.2.1(@types/web@0.0.86)(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.86)(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.86)(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 diff --git a/src/App.tsx b/src/App.tsx index 7d62c75..5962054 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,45 +1,39 @@ -import { - LoadingSpinner, - useDeskproAppEvents, - useDeskproLatestAppContext, -} from "@deskpro/app-sdk"; -import { Button, Stack, AnyIcon } from "@deskpro/deskpro-ui"; -import { faRefresh } from "@fortawesome/free-solid-svg-icons"; -import { Suspense } from "react"; -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; -import { ErrorBoundary } from "react-error-boundary"; -import { QueryClientProvider, QueryErrorResetBoundary } from "react-query"; -import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; -import { Organization } from "./pages/Organization"; -import { Ticket } from "./pages/Ticket"; -import { User } from "./pages/User"; -import { GlobalSignIn } from "./pages/admin/GlobalSignIn"; -import { Account } from "./pages/admin/mapping/Account"; -import { Contact } from "./pages/admin/mapping/Contact"; -import { Event } from "./pages/admin/mapping/Event"; -import { Lead } from "./pages/admin/mapping/Lead"; -import { Note } from "./pages/admin/mapping/Note"; -import { Opportunity } from "./pages/admin/mapping/Opportunity"; -import { Task } from "./pages/admin/mapping/Task"; -import { List } from "./pages/list/List"; -import { View } from "./pages/view/View"; -import { query } from "./query"; - import "./App.css"; - +import "@deskpro/deskpro-ui/dist/deskpro-custom-icons.css"; +import "@deskpro/deskpro-ui/dist/deskpro-ui.css"; import "flatpickr/dist/themes/light.css"; import "simplebar/dist/simplebar.min.css"; import "tippy.js/dist/tippy.css"; - -import "@deskpro/deskpro-ui/dist/deskpro-custom-icons.css"; -import "@deskpro/deskpro-ui/dist/deskpro-ui.css"; -import { ScrollTop } from "./components/ScrollTop"; +import { Account } from "./pages/admin/mapping/Account"; +import { AdminCallbackPage } from "./pages/admin/callback/AdminCallbackPage"; +import { Button, Stack, AnyIcon } from "@deskpro/deskpro-ui"; +import { Contact } from "./pages/admin/mapping/Contact"; import { CreateActivity } from "./pages/createEdit/Activity"; import { CreateNote } from "./pages/createEdit/Note"; import { CreateOpportunity } from "./pages/createEdit/Opportunity"; +import { DndProvider } from "react-dnd"; import { EditProfile } from "./pages/createEdit/Profile"; +import { ErrorBoundary } from "react-error-boundary"; +import { Event } from "./pages/admin/mapping/Event"; +import { faRefresh } from "@fortawesome/free-solid-svg-icons"; +import { GlobalSignIn } from "./pages/admin/GlobalSignIn"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { Lead } from "./pages/admin/mapping/Lead"; +import { List } from "./pages/list/List"; +import { Note } from "./pages/admin/mapping/Note"; +import { Opportunity } from "./pages/admin/mapping/Opportunity"; +import { Organization } from "./pages/Organization"; import { parseJsonErrorMessage } from "./utils"; +import { query } from "./query"; +import { QueryClientProvider, QueryErrorResetBoundary } from "react-query"; +import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; +import { ScrollTop } from "./components/ScrollTop"; +import { Suspense } from "react"; +import { Task } from "./pages/admin/mapping/Task"; +import { Ticket } from "./pages/Ticket"; +import { User } from "./pages/User"; +import { View } from "./pages/view/View"; +import {LoadingSpinner,useDeskproAppEvents,useDeskproLatestAppContext} from "@deskpro/app-sdk"; function App() { const { context } = useDeskproLatestAppContext(); @@ -94,33 +88,34 @@ function App() { } /> - }/> + } /> } /> } /> - }/> - }/> - }/> - }/> - }/> + } /> + } /> + } /> + } /> + } /> } /> - }/> + } /> } /> } /> - }/> + } /> } /> + } /> } /> } /> diff --git a/src/api/getAccessAndRefreshTokens.ts b/src/api/getAccessAndRefreshTokens.ts new file mode 100644 index 0000000..9d9154c --- /dev/null +++ b/src/api/getAccessAndRefreshTokens.ts @@ -0,0 +1,31 @@ +import { adminGenericProxyFetch, IDeskproClient, OAuth2Result } from "@deskpro/app-sdk"; +import { Settings } from "../types"; + +interface GetAccessAndRefreshTokensParams { + settings: Settings, + accessCode: string, + callbackUrl: string, + client: IDeskproClient +} + +export default async function getAccessAndRefreshTokens(props: GetAccessAndRefreshTokensParams): Promise { + const { settings, accessCode, callbackUrl, client } = props + + const fetch = await adminGenericProxyFetch(client); + + const requestOptions: RequestInit = { + method: "POST", + body: new URLSearchParams({ + grant_type: "authorization_code", + code: accessCode, + client_id: settings.client_key ?? "", + client_secret: settings?.client_secret ?? "", + redirect_uri: callbackUrl ?? "", + }), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }; + + return await fetch(`${settings?.salesforce_instance_url}/services/oauth2/token`, requestOptions).then((res) => res.json()); +} \ No newline at end of file diff --git a/src/api/preInstallationApi.ts b/src/api/preInstallationApi.ts index 13edff5..9917b3b 100644 --- a/src/api/preInstallationApi.ts +++ b/src/api/preInstallationApi.ts @@ -1,8 +1,8 @@ import { adminGenericProxyFetch, IDeskproClient } from "@deskpro/app-sdk"; -import { Settings } from "../types"; import { AuthTokens, ObjectMeta, RequestMethod } from "./types"; import { every, trimEnd } from "lodash"; import { isResponseError } from "./api"; +import { Settings } from "../types"; /** * Get current user details (whilst app is not installed) @@ -96,14 +96,12 @@ const preInstalledRequest = async ( ); // If our access token has expired, attempt to get a new one using the refresh token - if ([400, 401].includes(response.status)) { + const mode = settings.use_deskpro_saas ? "global" : "local"; + + if ([400, 401].includes(response.status) && mode === "local") { const refreshRequestOptions: RequestInit = { method: "POST", - body: `grant_type=refresh_token&client_id=${ - settings?.client_key as string - }&client_secret=${settings?.client_secret as string}&refresh_token=${ - tokens.refreshToken as string - }`, + body: `grant_type=refresh_token&client_id=${settings?.client_key}&client_secret=${settings?.client_secret}&refresh_token=${tokens.refreshToken}`, headers: { "Content-Type": "application/x-www-form-urlencoded", }, diff --git a/src/api/types.ts b/src/api/types.ts index 7be5681..6255442 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -3,7 +3,7 @@ export type RequestMethod = "GET" | "POST" | "PATCH" | "DELETE"; export type AuthTokens = { accessToken: string; refreshToken: string; -}; +} export type ObjectType = | "Contact" diff --git a/src/pages/admin/GlobalSignIn.tsx b/src/pages/admin/GlobalSignIn.tsx index 96fcd91..f9c8ce1 100644 --- a/src/pages/admin/GlobalSignIn.tsx +++ b/src/pages/admin/GlobalSignIn.tsx @@ -1,36 +1,22 @@ -import { - useDeskproAppTheme, - CopyToClipboardInput, -} from "@deskpro/app-sdk"; +import "./style.css"; +import { AnchorButton, Button, H2, P1 } from "@deskpro/deskpro-ui"; import { faSignIn, faSignOut } from "@fortawesome/free-solid-svg-icons"; -import { AnchorButton, Button, H2, P1, Spinner, Stack } from "@deskpro/deskpro-ui"; +import { useDeskproAppTheme } from "@deskpro/app-sdk"; import { useGlobalSignIn } from "./useGlobalSignIn"; -import "./style.css"; export const GlobalSignIn = () => { const { theme } = useDeskproAppTheme(); - const { - callbackUrl, user, - oAuthUrl, + authUrl, isLoading, isDisabled, - isBlocking, isInstanceUrlInvalid, cancelLoading, signIn, signOut, } = useGlobalSignIn(); - if (isBlocking) { - return ( - - - - ); - } - return ( <> {isInstanceUrlInvalid && ( @@ -40,16 +26,6 @@ export const GlobalSignIn = () => { )} - {callbackUrl && ( - <> -

Callback URL

- - - The callback URL will be required during Salesforce app setup - - - )} -

Global Salesforce User*

{user ? ( @@ -65,13 +41,13 @@ export const GlobalSignIn = () => { This Salesforce user account will be used by all Deskpro agents diff --git a/src/pages/admin/callback/AdminCallbackPage.tsx b/src/pages/admin/callback/AdminCallbackPage.tsx new file mode 100644 index 0000000..f904b6e --- /dev/null +++ b/src/pages/admin/callback/AdminCallbackPage.tsx @@ -0,0 +1,36 @@ +import { CopyToClipboardInput, LoadingSpinner, OAuth2Result, useInitialisedDeskproAppClient, } from "@deskpro/app-sdk"; +import { P1 } from "@deskpro/deskpro-ui"; +import { useState } from "react"; +import type { FC } from "react"; + +const AdminCallbackPage: FC = () => { + const [callbackUrl, setCallbackUrl] = useState(null); + + useInitialisedDeskproAppClient(async (client) => { + const oauth2 = await client.startOauth2Local( + ({ callbackUrl, state }) => `https://test.my.salesforce.com/services/oauth2/authorize?response_type=code&client_id=xx&redirect_uri=${callbackUrl}&state=${state}&scope=${"refresh_token api"}`, + /code=(?[0-9a-f]+)/, + async (): Promise => ({ data: { access_token: "", refresh_token: "" } }) + ); + + const url = new URL(oauth2.authorizationUrl); + const redirectUri = url.searchParams.get("redirect_uri"); + + if (redirectUri) { + setCallbackUrl(redirectUri); + } + }); + + if (!callbackUrl) { + return (); + } + + return ( + <> + + The callback URL will be required during your Salesforce app setup + + ); +}; + +export { AdminCallbackPage } \ No newline at end of file diff --git a/src/pages/admin/useGlobalSignIn.ts b/src/pages/admin/useGlobalSignIn.ts index 8da8851..cf28601 100644 --- a/src/pages/admin/useGlobalSignIn.ts +++ b/src/pages/admin/useGlobalSignIn.ts @@ -1,186 +1,141 @@ -import { - adminGenericProxyFetch, - useDeskproAppClient, - useDeskproAppEvents, - useInitialisedDeskproAppClient -} from "@deskpro/app-sdk"; -import { useEffect, useMemo, useState } from "react"; -import { Settings } from "../../types"; -import { v4 as uuidv4 } from "uuid"; -import { every, isEmpty } from "lodash"; -import { AuthTokens } from "../../api/types"; import { getMePreInstalled } from "../../api/preInstallationApi"; +import { IOAuth2, OAuth2Result, useDeskproAppClient, useDeskproLatestAppContext, useInitialisedDeskproAppClient } from "@deskpro/app-sdk"; +import { Settings } from "../../types"; +import { useCallback, useState } from "react"; +import getAccessAndRefreshTokens from "../../api/getAccessAndRefreshTokens"; +interface User { + name: string + email: string +} export const useGlobalSignIn = () => { const { client } = useDeskproAppClient(); - const [ settings, setSettings ] = useState(null); - const [ callbackUrl, setCallbackUrl ] = useState(null); - const [ poll, setPoll ] = useState<(() => Promise<{ token: string }>)|null>(null); - const [ isLoading, setIsLoading ] = useState(false); - const [ isBlocking, setIsBlocking ] = useState(true); - const [ accessCode, setAccessCode ] = useState(null); - const [ user, setUser ] = useState<{ name: string; email: string; }|null>(null); - - const key = useMemo(() => uuidv4(), []); + const [user, setUser] = useState(null); + const [authUrl, setAuthUrl] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isPolling, setIsPolling] = useState(false) + const [oauth2Context, setOAuth2Context] = useState(null) + const [error, setError] = useState(null); + const { context } = useDeskproLatestAppContext(); + + const settings = context?.settings + + useInitialisedDeskproAppClient(async (client) => { + if (!settings || context?.settings.use_deskpro_saas === undefined) { + // Make sure settings have loaded. + return + } - useDeskproAppEvents({ - onAdminSettingsChange: setSettings, - }, []); + const clientId = settings.client_key; + const mode = settings.use_deskpro_saas ? "global" : "local"; - // Initialise OAuth flow - useInitialisedDeskproAppClient((client) => { - (async () => { - const { callbackUrl, poll } = await client.oauth2().getAdminGenericCallbackUrl( - key, - /\?code=(?.+?)&/, - /&state=(?.+)/ - ); - - setCallbackUrl(callbackUrl); - setPoll(() => poll); - })(); - }, [key]); - - // Build auth flow entrypoint URL - const oAuthUrl = useMemo(() => { - if (!every([settings?.salesforce_instance_url, settings?.client_key])) { - return null; + // Ensure the is a client id if in local mode + if (mode === "local" && !clientId) { + // Reset the authURL because there might be an authURL from when + // the user was in global mode + setAuthUrl(null) + return } - const url = new URL(`${settings?.salesforce_instance_url}/services/oauth2/authorize`); - - url.search = new URLSearchParams({ - response_type: "code", - client_id: settings?.client_key as string, - redirect_uri: callbackUrl as string, - state: key, - scope: "refresh_token api", - }).toString(); - - return url; - }, [ - settings?.salesforce_instance_url, - settings?.client_key, - callbackUrl, - key - ]); - - // Exchange auth code for auth/refresh tokens + const oAuth2Response = + mode === "local" + // Local Version (custom/self-hosted app) + ? await client.startOauth2Local( + ({ state, callbackUrl }) => { + return `${settings?.salesforce_instance_url}/services/oauth2/authorize?response_type=code&client_id=${clientId}&redirect_uri=${callbackUrl}&state=${state}&scope=${"refresh_token api"}`; + }, + /\?code=(?.+?)&/, + async (code: string): Promise => { + const url = new URL(oAuth2Response.authorizationUrl); + const redirectUri = url.searchParams.get("redirect_uri"); + if (!redirectUri) throw new Error("Failed to get callback URL"); + + const data = await getAccessAndRefreshTokens({ settings, accessCode: code, callbackUrl: redirectUri, client }) + return { data }; + } + ) + // Global Proxy Service + : await client.startOauth2Global("3MVG9k02hQhyUgQBDJFGjHpunit6Qn7nRoDm5DY06FG..mnbGEq316N2sOU4I4qZVculsUMYaTad8cY7.0gfV"); + + setAuthUrl(oAuth2Response.authorizationUrl); + setOAuth2Context(oAuth2Response); + }, [context, settings]) + useInitialisedDeskproAppClient((client) => { - const canRequestAccessToken = every([ - accessCode, - callbackUrl, - settings?.salesforce_instance_url, - settings?.client_key, - settings?.client_secret, - ]); - - if (!canRequestAccessToken) { - return; + if (!oauth2Context || !settings) { + return } - const url = new URL(`${settings?.salesforce_instance_url}/services/oauth2/token`); - - const requestOptions: RequestInit = { - method: "POST", - body: new URLSearchParams({ - grant_type: "authorization_code", - code: accessCode as string, - client_id: settings?.client_key as string, - client_secret: settings?.client_secret as string, - redirect_uri: callbackUrl as string, - }), - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }; - - (async () => { - const fetch = await adminGenericProxyFetch(client); - const response = await fetch(url.toString(), requestOptions); - const data = await response.json(); - - const tokens: AuthTokens = { - accessToken: data.access_token, - refreshToken: data.refresh_token, - }; - - client.setAdminSetting(JSON.stringify(tokens)); - - setIsLoading(false); - })(); - }, [ - accessCode, - callbackUrl, - settings?.salesforce_instance_url, - settings?.client_key, - settings?.client_secret, - ]); - - // Get current Salesforce user - useInitialisedDeskproAppClient((client) => { - (async () => { - if (!isEmpty(settings?.global_access_token) && settings?.salesforce_instance_url) { - setUser(await getMePreInstalled(client, settings)); + const startPolling = async () => { + + try { + const result = await oauth2Context.poll() + + // Update the access/refresh tokens + const stringifiedTokens = JSON.stringify(result.data) + client.setAdminSetting(stringifiedTokens); + + let currentUser: User | null = null + try { + currentUser = await getMePreInstalled(client, settings) + if (!currentUser) { + throw new Error() + } + } catch { + throw new Error("An error occurred while verifying the user") + } + + setUser(currentUser) + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setIsLoading(false) + setIsPolling(false) } - })(); - }, [settings?.global_access_token, settings?.salesforce_instance_url]); - - // Set blocking flag - useEffect(() => { - if (!(callbackUrl && client && poll)) { - setIsBlocking(true); - } else if (settings?.global_access_token && !user) { - setIsBlocking(true); - } else { - setIsBlocking(false); } - }, [ - callbackUrl, - client, - poll, - user, - settings?.global_access_token - ]); + + if (isPolling) { + void startPolling() + } + }, [isPolling, oauth2Context, settings]) const signOut = () => { client?.setAdminSetting(""); setUser(null); - setAccessCode(null); }; - const signIn = () => { - poll && (async () => { - setIsLoading(true); - setAccessCode((await poll()).token) - })(); - }; + const signIn = useCallback(() => { + setIsLoading(true) + setIsPolling(true) + window.open(authUrl ?? "", '_blank'); + }, [setIsLoading, authUrl]); // Only enable the sign-in button once we have all necessary settings - let isDisabled = ! every([ - settings?.client_key, - settings?.client_secret, - settings?.salesforce_instance_url, - ]); + let isDisabled = false + + if (settings && settings.use_deskpro_saas === false && !settings.client_key || !settings?.client_secret || !settings.salesforce_instance_url) { + isDisabled = true + } const isInstanceUrlInvalid = settings?.salesforce_instance_url // eslint-disable-next-line no-useless-escape ? !/https:\/\/[a-zA-Z0-9\-]+\.(sandbox|develop\.)?my\.salesforce\.com$/.test(settings.salesforce_instance_url) : false - ; if (settings?.salesforce_instance_url && isInstanceUrlInvalid) { isDisabled = true; } - const cancelLoading = () => setIsLoading(false); + const cancelLoading = () => { + setIsLoading(false) + setIsPolling(false) + }; return { - callbackUrl, user, - oAuthUrl, isLoading, - isBlocking, + error, + authUrl, isDisabled, isInstanceUrlInvalid, cancelLoading, diff --git a/src/types.ts b/src/types.ts index fac7a6a..429f82e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ import { HomeLayout, ListLayout, ViewLayout } from "./screens/admin/types"; export interface Settings { client_key?: string; client_secret?: string; + use_deskpro_saas?: boolean, salesforce_instance_url?: string; global_access_token?: string; mapping_contact?: string;