diff --git a/packages/twenty-chrome-extension/src/background/index.ts b/packages/twenty-chrome-extension/src/background/index.ts index 69ae83cc2d5e..a5d30b2a2f5b 100644 --- a/packages/twenty-chrome-extension/src/background/index.ts +++ b/packages/twenty-chrome-extension/src/background/index.ts @@ -1,6 +1,3 @@ -import Crypto from 'crypto-js'; - -import { exchangeAuthorizationCode } from '~/db/auth.db'; import { isDefined } from '~/utils/isDefined'; // Open options page programmatically in a new tab. @@ -25,12 +22,6 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => { }); break; } - case 'launchOAuth': { - launchOAuth(({ status, message }) => { - sendResponse({ status, message }); - }); - break; - } case 'openSidepanel': { chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { if (isDefined(tab) && isDefined(tab.id)) { @@ -57,84 +48,6 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => { return true; }); -const generateRandomString = (length: number) => { - const charset = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; - let result = ''; - for (let i = 0; i < length; i++) { - result += charset.charAt(Math.floor(Math.random() * charset.length)); - } - return result; -}; - -const generateCodeVerifierAndChallenge = () => { - const codeVerifier = generateRandomString(32); - const hash = Crypto.SHA256(codeVerifier); - const codeChallenge = hash - .toString(Crypto.enc.Base64) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); - - return { codeVerifier, codeChallenge }; -}; - -const launchOAuth = ( - callback: ({ status, message }: { status: boolean; message: string }) => void, -) => { - const { codeVerifier, codeChallenge } = generateCodeVerifierAndChallenge(); - const redirectUrl = chrome.identity.getRedirectURL(); - chrome.identity - .launchWebAuthFlow({ - url: `${ - import.meta.env.VITE_FRONT_BASE_URL - }/authorize?clientId=chrome&codeChallenge=${codeChallenge}&redirectUrl=${redirectUrl}`, - interactive: true, - }) - .then((responseUrl) => { - if (typeof responseUrl === 'string') { - const url = new URL(responseUrl); - const authorizationCode = url.searchParams.get( - 'authorizationCode', - ) as string; - exchangeAuthorizationCode({ - authorizationCode, - codeVerifier, - }).then((tokens) => { - if (isDefined(tokens)) { - chrome.storage.local.set({ - loginToken: tokens.loginToken, - }); - - chrome.storage.local.set({ - accessToken: tokens.accessToken, - }); - - chrome.storage.local.set({ - refreshToken: tokens.refreshToken, - }); - - callback({ status: true, message: '' }); - - chrome.tabs.query( - { active: true, currentWindow: true }, - ([tab]) => { - if (isDefined(tab) && isDefined(tab.id)) { - chrome.tabs.sendMessage(tab.id, { - action: 'executeContentScript', - }); - } - }, - ); - } - }); - } - }) - .catch((error) => { - callback({ status: false, message: error.message }); - }); -}; - chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => { const isDesiredRoute = tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) || @@ -154,3 +67,34 @@ chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => { enabled: true, }); }); + +const setTokenStateFromCookie = (cookie: string) => { + const decodedValue = decodeURIComponent(cookie); + const tokenPair = JSON.parse(decodedValue); + if (isDefined(tokenPair)) { + chrome.storage.local.set({ + isAuthenticated: true, + accessToken: tokenPair.accessToken, + refreshToken: tokenPair.refreshToken, + }); + } +}; + +chrome.cookies.onChanged.addListener(async ({ cookie }) => { + if (cookie.name === 'tokenPair') { + setTokenStateFromCookie(cookie.value); + } +}); + +// This will only run the very first time the extension loads, after we have stored the +// cookiesRead variable to true, this will not allow to change the token state everytime background script runs +chrome.cookies.get( + { name: 'tokenPair', url: `${import.meta.env.VITE_FRONT_BASE_URL}` }, + async (cookie) => { + const store = await chrome.storage.local.get(['cookiesRead']); + if (isDefined(cookie) && !isDefined(store.cookiesRead)) { + setTokenStateFromCookie(cookie.value); + chrome.storage.local.set({ cookiesRead: true }); + } + }, +); diff --git a/packages/twenty-chrome-extension/src/contentScript/index.ts b/packages/twenty-chrome-extension/src/contentScript/index.ts index 2699d3a1fc22..7bf17bb09458 100644 --- a/packages/twenty-chrome-extension/src/contentScript/index.ts +++ b/packages/twenty-chrome-extension/src/contentScript/index.ts @@ -1,5 +1,6 @@ import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile'; import { insertButtonForPerson } from '~/contentScript/extractPersonProfile'; +import { isDefined } from '~/utils/isDefined'; // Inject buttons into the DOM when SPA is reloaded on the resource url. // e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/ @@ -21,3 +22,12 @@ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => { sendResponse('Executing!'); }); + +chrome.storage.local.onChanged.addListener(async (store) => { + if (isDefined(store.accessToken)) { + if (isDefined(store.accessToken.newValue)) { + await insertButtonForCompany(); + await insertButtonForPerson(); + } + } +}); diff --git a/packages/twenty-chrome-extension/src/manifest.ts b/packages/twenty-chrome-extension/src/manifest.ts index 18f21d764c2b..39b15c790fe1 100644 --- a/packages/twenty-chrome-extension/src/manifest.ts +++ b/packages/twenty-chrome-extension/src/manifest.ts @@ -2,10 +2,6 @@ import { defineManifest } from '@crxjs/vite-plugin'; import packageData from '../package.json'; -const host_permissions = - process.env.VITE_MODE === 'development' - ? ['https://www.linkedin.com/*', 'http://localhost:3001/*'] - : ['https://www.linkedin.com/*']; const external_sites = process.env.VITE_MODE === 'development' ? [`https://app.twenty.com/*`, `http://localhost:3001/*`] @@ -48,9 +44,12 @@ export default defineManifest({ }, ], - permissions: ['activeTab', 'storage', 'identity', 'sidePanel'], + permissions: ['activeTab', 'storage', 'identity', 'sidePanel', 'cookies'], - host_permissions: host_permissions, + // setting host permissions to all http connections will allow + // for people who host on their custom domain to get access to + // extension instead of white listing individual urls + host_permissions: ['https://*/*', 'http://*/*'], externally_connectable: { matches: external_sites, diff --git a/packages/twenty-chrome-extension/src/options/Settings.tsx b/packages/twenty-chrome-extension/src/options/Settings.tsx new file mode 100644 index 000000000000..1df9ae9e65f4 --- /dev/null +++ b/packages/twenty-chrome-extension/src/options/Settings.tsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from 'react'; +import styled from '@emotion/styled'; + +import { TextInput } from '@/ui/input/components/TextInput'; +import { isDefined } from '~/utils/isDefined'; + +const StyledWrapper = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.primary}; + display: flex; + height: 100vh; + justify-content: center; +`; + +const StyledContainer = styled.div` + width: 400px; + height: 350px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledActionContainer = styled.div` + align-items: center; + display: flex; + flex-direction: column; + gap: 10px; + justify-content: center; + width: 300px; +`; + +const Settings = () => { + const [serverBaseUrl, setServerBaseUrl] = useState(''); + const [clientUrl, setClientUrl] = useState(''); + + useEffect(() => { + const getState = async () => { + const store = await chrome.storage.local.get(); + if (isDefined(store.serverBaseUrl)) { + setServerBaseUrl(store.serverBaseUrl); + } else { + setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL); + } + + if (isDefined(store.clientUrl)) { + setClientUrl(store.clientUrl); + } else { + setClientUrl(import.meta.env.VITE_FRONT_BASE_URL); + } + }; + void getState(); + }, []); + + const handleBaseUrlChange = (value: string) => { + setServerBaseUrl(value); + chrome.storage.local.set({ serverBaseUrl: value }); + }; + + const handleClientUrlChange = (value: string) => { + setClientUrl(value); + chrome.storage.local.set({ clientUrl: value }); + }; + + return ( + + + twenty-logo + + + + + + + ); +}; + +export default Settings; diff --git a/packages/twenty-chrome-extension/src/options/Sidepanel.tsx b/packages/twenty-chrome-extension/src/options/Sidepanel.tsx index 674a3f799898..7632bda49a09 100644 --- a/packages/twenty-chrome-extension/src/options/Sidepanel.tsx +++ b/packages/twenty-chrome-extension/src/options/Sidepanel.tsx @@ -1,9 +1,7 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import styled from '@emotion/styled'; -import { Loader } from '@/ui/display/loader/components/Loader'; import { MainButton } from '@/ui/input/button/MainButton'; -import { TextInput } from '@/ui/input/components/TextInput'; import { isDefined } from '~/utils/isDefined'; const StyledIframe = styled.iframe` @@ -41,126 +39,73 @@ const StyledActionContainer = styled.div` `; const Sidepanel = () => { - const [isAuthenticating, setIsAuthenticating] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false); - const [iframeSrc, setIframeSrc] = useState( + const [clientUrl, setClientUrl] = useState( import.meta.env.VITE_FRONT_BASE_URL, ); - const [error, setError] = useState(''); - const [serverBaseUrl, setServerBaseUrl] = useState(''); - const authenticate = () => { - setIsAuthenticating(true); - setError(''); - chrome.runtime.sendMessage( - { action: 'launchOAuth' }, - ({ status, message }) => { - if (status === true) { - setIsAuthenticated(true); - setIsAuthenticating(false); - chrome.storage.local.set({ isAuthenticated: true }); - } else { - setError(message); - setIsAuthenticating(false); - } - }, - ); - }; + const iframeRef = useRef(null); + + const setIframeState = useCallback(async () => { + const store = await chrome.storage.local.get(); + if (isDefined(store.isAuthenticated)) setIsAuthenticated(true); + const { tab: activeTab } = await chrome.runtime.sendMessage({ + action: 'getActiveTab', + }); + + if ( + isDefined(activeTab) && + isDefined(store[`sidepanelUrl_${activeTab.id}`]) + ) { + const url = store[`sidepanelUrl_${activeTab.id}`]; + setClientUrl(url); + } else if (isDefined(store.clientUrl)) { + setClientUrl(store.clientUrl); + } + }, [setClientUrl]); useEffect(() => { - const getState = async () => { + const initState = async () => { const store = await chrome.storage.local.get(); - if (isDefined(store.serverBaseUrl)) { - setServerBaseUrl(store.serverBaseUrl); - } else { - setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL); - } - - if (store.isAuthenticated === true) setIsAuthenticated(true); - const { tab: activeTab } = await chrome.runtime.sendMessage({ - action: 'getActiveTab', - }); - - if ( - isDefined(activeTab) && - isDefined(store[`sidepanelUrl_${activeTab.id}`]) - ) { - const url = store[`sidepanelUrl_${activeTab.id}`]; - setIframeSrc(url); - } + if (isDefined(store.isAuthenticated)) setIsAuthenticated(true); + void setIframeState(); }; - void getState(); + void initState(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - const handleBrowserEvents = ({ action }: { action: string }) => { - if (action === 'changeSidepanelUrl') { - setIframeSrc(''); - } - }; - chrome.runtime.onMessage.addListener(handleBrowserEvents); - - return () => { - chrome.runtime.onMessage.removeListener(handleBrowserEvents); - }; - }, []); + void setIframeState(); + }, [setIframeState, clientUrl]); useEffect(() => { - const getIframeState = async () => { - const store = await chrome.storage.local.get(); - const { tab: activeTab } = await chrome.runtime.sendMessage({ - action: 'getActiveTab', - }); - - if ( - isDefined(activeTab) && - isDefined(store[`sidepanelUrl_${activeTab.id}`]) - ) { - const url = store[`sidepanelUrl_${activeTab.id}`]; - setIframeSrc(url); + chrome.storage.local.onChanged.addListener((store) => { + if (isDefined(store.isAuthenticated)) { + if (store.isAuthenticated.newValue === true) { + setIframeState(); + } } - }; - void getIframeState(); - }, [iframeSrc]); - - const handleBaseUrlChange = (value: string) => { - setServerBaseUrl(value); - setError(''); - chrome.storage.local.set({ serverBaseUrl: value }); - }; + }); + }, [setIframeState]); return isAuthenticated ? ( - + ) : ( twenty-logo - {isAuthenticating ? ( - - ) : ( - - - authenticate()} - fullWidth - /> - - window.open(`${import.meta.env.VITE_FRONT_BASE_URL}`, '_blank') - } - fullWidth - /> - - )} + + { + window.open(clientUrl, '_blank'); + }} + /> + );