From 9537a3b330cfca80ed90192857879ba62854db4c Mon Sep 17 00:00:00 2001 From: mit-27 Date: Mon, 18 Nov 2024 01:25:39 -0700 Subject: [PATCH 1/2] Integrate magic-link in webapp by /magic-link route --- .../src/app/(Magic-Link)/magic-link/page.tsx | 605 ++++++++++++++++++ .../components/Connection/ConnectionTable.tsx | 195 +++--- .../components/Connection/CopyLinkInput.tsx | 92 +-- .../components/magic-link/custom-modal.tsx | 43 ++ .../magic-link/useCreateApiKeyConnection.tsx | 50 ++ apps/webapp/src/hooks/magic-link/useOAuth.ts | 97 +++ .../hooks/magic-link/useProjectConnectors.tsx | 24 + .../hooks/magic-link/useUniqueMagicLink.tsx | 27 + apps/webapp/src/lib/config.ts | 1 + 9 files changed, 1015 insertions(+), 119 deletions(-) create mode 100644 apps/webapp/src/app/(Magic-Link)/magic-link/page.tsx create mode 100644 apps/webapp/src/components/magic-link/custom-modal.tsx create mode 100644 apps/webapp/src/hooks/magic-link/useCreateApiKeyConnection.tsx create mode 100644 apps/webapp/src/hooks/magic-link/useOAuth.ts create mode 100644 apps/webapp/src/hooks/magic-link/useProjectConnectors.tsx create mode 100644 apps/webapp/src/hooks/magic-link/useUniqueMagicLink.tsx diff --git a/apps/webapp/src/app/(Magic-Link)/magic-link/page.tsx b/apps/webapp/src/app/(Magic-Link)/magic-link/page.tsx new file mode 100644 index 000000000..80003727a --- /dev/null +++ b/apps/webapp/src/app/(Magic-Link)/magic-link/page.tsx @@ -0,0 +1,605 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useSearchParams } from "next/navigation"; +import Modal from "@/components/magic-link/custom-modal"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import config from "@/lib/config"; +import useCreateApiKeyConnection from "@/hooks/magic-link/useCreateApiKeyConnection"; +import useProjectConnectors from "@/hooks/magic-link/useProjectConnectors"; +import useUniqueMagicLink from "@/hooks/magic-link/useUniqueMagicLink"; +import useOAuth from "@/hooks/magic-link/useOAuth"; +import { + AuthStrategy, + categoryFromSlug, + CONNECTORS_METADATA, + Provider, + providersArray, +} from "@panora/shared/src"; +import { categoriesVerticals } from "@panora/shared/src/categories"; +import { ArrowLeft, ArrowLeftRight, Search, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; + +interface IBasicAuthFormData { + [key: string]: string; +} + +const domainFormats: { [key: string]: string } = { + salesforce: + "If your Salesforce site URL is https://acme-dev.lightning.force.com, acme-dev is your domain", + sharepoint: + "If the SharePoint site URL is https://joedoe.sharepoint.com/sites/acme-dev, joedoe is the tenant and acme-dev is the site name.", + microsoftdynamicssales: + "If your Microsoft Dynamics URL is acme-dev.api.crm3.dynamics.com then acme-dev is the organization name.", + bigcommerce: + "If your api domain is https://api.bigcommerce.com/stores/joehash123/v3 then store_hash is joehash123.", +}; + +const ProviderModal = () => { + const searchParamas = useSearchParams(); + const router = useRouter(); + const [currentURL, setCurrentURL] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("All"); + const [selectedProvider, setSelectedProvider] = useState<{ + provider: string; + category: string; + }>({ provider: "", category: "" }); + const [startFlow, setStartFlow] = useState(false); + const [preStartFlow, setPreStartFlow] = useState(false); + const [openBasicAuthDialog, setOpenBasicAuthDialog] = + useState(false); + const [projectId, setProjectId] = useState(""); + const [data, setData] = useState([]); + const [isProjectIdReady, setIsProjectIdReady] = useState(false); + const [errorResponse, setErrorResponse] = useState<{ + errorPresent: boolean; + errorMessage: string; + }>({ errorPresent: false, errorMessage: "" }); + const [loading, setLoading] = useState<{ + status: boolean; + provider: string; + }>({ status: false, provider: "" }); + const [additionalParams, setAdditionalParams] = useState<{ + [key: string]: string; + }>({}); + const [uniqueMagicLinkId, setUniqueMagicLinkId] = useState( + null + ); + const [openSuccessDialog, setOpenSuccessDialog] = useState(false); + const [currentProviderLogoURL, setCurrentProviderLogoURL] = + useState(""); + const [currentProvider, setCurrentProvider] = useState(""); + const [redirectIngressUri, setRedirectIngressUri] = useState<{ + status: boolean; + value: string | null; + }>({ + status: false, + value: null, + }); + const { mutate: createApiKeyConnection } = useCreateApiKeyConnection(); + const { data: magicLink } = useUniqueMagicLink(uniqueMagicLinkId); + const { data: connectorsForProject } = useProjectConnectors( + isProjectIdReady ? projectId : null + ); + const { + register: register2, + formState: { errors: errors2 }, + handleSubmit: handleSubmit2, + reset: reset2, + } = useForm(); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + // const queryParams = new URLSearchParams(window.location.search); + const uniqueId = searchParamas.get("uniqueLink"); + const param = searchParamas.get("redirectIngressUri"); + if (uniqueId) { + setUniqueMagicLinkId(uniqueId); + } + if (param !== null && param !== undefined) { + setRedirectIngressUri({ + status: true, + value: param, + }); + } + if (typeof window !== "undefined") { + setCurrentURL(window.location.href); + } + }, []); + + useEffect(() => { + if (magicLink) { + setProjectId(magicLink?.id_project); + setIsProjectIdReady(true); + } + }, [magicLink]); + + useEffect(() => { + if (isProjectIdReady && connectorsForProject) { + const PROVIDERS = + selectedCategory == "All" + ? providersArray() + : providersArray(selectedCategory); + const getConnectorsToDisplay = () => { + // First, check if the company selected custom connectors in the UI or not + const unwanted_connectors = transformConnectorsStatus( + connectorsForProject + ).filter((connector) => connector.status === "false"); + // Filter out the providers present in the unwanted connectors array + const filteredProviders = PROVIDERS.filter((provider) => { + return !unwanted_connectors.some( + (unwanted) => + unwanted.category === provider.vertical && + unwanted.connector_name === provider.name + ); + }); + return filteredProviders; + }; + setData(getConnectorsToDisplay()); + } + }, [connectorsForProject, selectedCategory, isProjectIdReady]); + + const { open, isReady } = useOAuth({ + providerName: selectedProvider?.provider!, + vertical: selectedProvider?.category!, + returnUrl: currentURL, + projectId: projectId, + linkedUserId: magicLink?.id_linked_user as string, + redirectIngressUri, + onSuccess: () => { + console.log("OAuth successful"); + setOpenSuccessDialog(true); + }, + additionalParams, + }); + + const onWindowClose = () => { + setSelectedProvider({ + provider: "", + category: "", + }); + + setLoading({ + status: false, + provider: "", + }); + setStartFlow(false); + setPreStartFlow(false); + }; + + useEffect(() => { + if (startFlow && isReady) { + setErrorResponse({ errorPresent: false, errorMessage: "" }); + open(onWindowClose).catch((error: Error) => { + setLoading({ + status: false, + provider: "", + }); + setErrorResponse({ errorPresent: true, errorMessage: error.message }); + setStartFlow(false); + setPreStartFlow(false); + }); + } else if (startFlow && !isReady) { + setLoading({ + status: false, + provider: "", + }); + } + }, [startFlow, isReady]); + + const CloseSuccessDialog = (close: boolean) => { + if (!close) { + setCurrentProvider(""); + setCurrentProviderLogoURL(""); + setOpenSuccessDialog(close); + } + }; + + function transformConnectorsStatus(connectors: { + [key: string]: boolean | null; + }): { connector_name: string; category: string; status: string }[] { + return Object.entries(connectors).flatMap(([key, value]) => { + const [category_slug, connector_name] = key + .split("_") + .map((part: string) => part.trim()); + const category = categoryFromSlug(category_slug); + if (category != null) { + return [ + { + connector_name: connector_name, + category: category, + status: value == null ? "true" : String(value), + }, + ]; + } + return []; + }); + } + + const onCloseBasicAuthDialog = (dialogState: boolean) => { + setOpenBasicAuthDialog(dialogState); + reset2(); + }; + + const onBasicAuthSubmit = (values: IBasicAuthFormData) => { + onCloseBasicAuthDialog(false); + setLoading({ status: true, provider: selectedProvider?.provider! }); + setPreStartFlow(false); + // Creating Basic Auth Connection + const providerMetadata = + CONNECTORS_METADATA[selectedProvider.category][selectedProvider.provider]; + + if (providerMetadata.authStrategy.strategy === AuthStrategy.oauth2) { + setAdditionalParams(values); + setStartFlow(true); + } else { + createApiKeyConnection( + { + query: { + linkedUserId: magicLink?.id_linked_user as string, + projectId: projectId, + providerName: selectedProvider?.provider!, + vertical: selectedProvider?.category!, + }, + data: values, + }, + { + onSuccess: () => { + setSelectedProvider({ + provider: "", + category: "", + }); + + setLoading({ + status: false, + provider: "", + }); + setOpenSuccessDialog(true); + }, + onError: (error) => { + setErrorResponse({ + errorPresent: true, + errorMessage: error.message, + }); + setLoading({ + status: false, + provider: "", + }); + setSelectedProvider({ + provider: "", + category: "", + }); + }, + } + ); + } + }; + + const filteredProviders = data.filter( + (provider) => + provider.name.toLowerCase().includes(searchTerm.toLowerCase()) && + (selectedCategory === "All" || + provider.vertical?.toLowerCase() === selectedCategory.toLowerCase()) + ); + + const handleProviderSelect = (provider: Provider) => { + setSelectedProvider({ + provider: provider.name.toLowerCase(), + category: provider.vertical!.toLowerCase(), + }); + const logoPath = + CONNECTORS_METADATA[provider.vertical!.toLowerCase()][ + provider.name.toLowerCase() + ].logoPath; + setCurrentProviderLogoURL(logoPath); + setCurrentProvider(provider.name.toLowerCase()); + + const providerMetadata = + CONNECTORS_METADATA[provider.vertical!.toLowerCase()][ + provider.name.toLowerCase() + ]; + if ( + providerMetadata.authStrategy.strategy === AuthStrategy.api_key || + providerMetadata.authStrategy.strategy === AuthStrategy.basic || + (providerMetadata.authStrategy.strategy === AuthStrategy.oauth2 && + providerMetadata.authStrategy.properties) + ) { + setOpenBasicAuthDialog(true); + } else { + setLoading({ status: true, provider: provider.name.toLowerCase() }); + setStartFlow(true); + } + }; + + const formatProvider = (provider: string) => { + return provider.substring(0, 1).toUpperCase() + provider.substring(1); + }; + const formatVertical = (vertical: string) => { + switch (vertical) { + case "marketingautomation": + return "Marketing Automation"; + case "filestorage": + return "File Storage"; + case "crm": + return "CRM"; + default: + return vertical.substring(0, 1).toUpperCase() + vertical.substring(1); + } + }; + + return ( +
+
+
+

+ Select Your Provider +

+ { + /* Close modal logic */ + }} + /> +
+ +
+
+ setSearchTerm(e.target.value)} + className="pl-10" + /> + +
+ +
+ +
+ {filteredProviders.map((provider) => ( +
handleProviderSelect(provider)} + > +
+ {provider.name} +
+ + {formatProvider(provider.name)} + +
+ ))} +
+
+ + {/* Basic Auth Dialog */} + + +
+ +
+ +
+ {selectedProvider?.category && + selectedProvider?.provider && + CONNECTORS_METADATA[selectedProvider.category]?.[ + selectedProvider.provider + ] && ( + <> +
+ {selectedProvider.provider} +
+

+ Connect your{" "} + {selectedProvider.provider.charAt(0).toUpperCase() + + selectedProvider.provider.slice(1)}{" "} + Account +

+ + )} + +
+ {selectedProvider.provider !== "" && + selectedProvider.category !== "" && + CONNECTORS_METADATA[selectedProvider.category][ + selectedProvider.provider + ].authStrategy.properties?.map((fieldName: string) => ( +
+ + {errors2[fieldName] && ( +

+ {errors2[fieldName]?.message} +

+ )} +
+ ))} + {domainFormats[selectedProvider.provider] && ( +

+ {domainFormats[selectedProvider.provider]} +

+ )} + + +
+
+
+
+ + {/* Success Dialog */} + +
+
+ +
+
+ + +
+ {currentProvider} +
+
+

+ Your data is being imported... +

+
+ + + You've successfully connected your account! + +
+ +
+
+
+ + {/* Loading state */} + {loading.status && ( +
+
+ + Connecting to {loading.provider}... +
+
+ )} + + {/* Error message */} + {errorResponse.errorPresent && ( +
+

{errorResponse.errorMessage}

+
+ )} +
+ ); +}; + +export default ProviderModal; diff --git a/apps/webapp/src/components/Connection/ConnectionTable.tsx b/apps/webapp/src/components/Connection/ConnectionTable.tsx index 539204a83..d285dac3b 100644 --- a/apps/webapp/src/components/Connection/ConnectionTable.tsx +++ b/apps/webapp/src/components/Connection/ConnectionTable.tsx @@ -1,14 +1,17 @@ -'use client' +"use client"; -import { columns } from "./columns" -import { DataTable } from "../shared/data-table" +import { columns } from "./columns"; +import { DataTable } from "../shared/data-table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog"; + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; import { Button } from "../ui/button"; import { PlusCircledIcon } from "@radix-ui/react-icons"; import CopyLinkInput from "./CopyLinkInput"; @@ -19,49 +22,47 @@ import AddConnectionButton from "./AddConnectionButton"; import config from "@/lib/config"; import useMagicLinkStore from "@/state/magicLinkStore"; import useOrganisationStore from "@/state/organisationStore"; -import { usePostHog } from 'posthog-js/react' +import { usePostHog } from "posthog-js/react"; import useProjectStore from "@/state/projectStore"; export default function ConnectionTable() { - - const {idProject} = useProjectStore(); + const { idProject } = useProjectStore(); const { data: connections, isLoading, error } = useConnections(); const [isGenerated, setIsGenerated] = useState(false); - const posthog = usePostHog() - - const {uniqueLink} = useMagicLinkStore(); - const {nameOrg} = useOrganisationStore(); + const posthog = usePostHog(); - if(isLoading){ - return ( - - ) + const { uniqueLink } = useMagicLinkStore(); + const { nameOrg } = useOrganisationStore(); + + if (isLoading) { + return ; } if (error) { console.log("error connections.."); - } - - const linkedConnections = (filter: string) => connections?.filter((connection) => connection.status == filter); + } + + const linkedConnections = (filter: string) => + connections?.filter((connection) => connection.status == filter); const ts = connections?.map((connection) => ({ - organisation: nameOrg, + organisation: nameOrg, app: connection.provider_slug, - vertical: connection.vertical, - category: connection.token_type, + vertical: connection.vertical, + category: connection.token_type, status: connection.status, - linkedUser: connection.id_linked_user, - date: connection.created_at, - connectionToken: connection.connection_token! - })) + linkedUser: connection.id_linked_user, + date: connection.created_at, + connectionToken: connection.connection_token!, + })); let link: string; - if(config.DISTRIBUTION == 'selfhost' && config.REDIRECT_WEBHOOK_INGRESS) { - link = `${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}&redirectIngressUri=${config.REDIRECT_WEBHOOK_INGRESS}` - }else{ - link = `${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}` + if (config.DISTRIBUTION == "selfhost" && config.REDIRECT_WEBHOOK_INGRESS) { + link = `${config.WEBAPP_URL}/magic-link?uniqueLink=${uniqueLink}&redirectIngressUri=${config.REDIRECT_WEBHOOK_INGRESS}`; + } else { + link = `${config.WEBAPP_URL}/magic-link?uniqueLink=${uniqueLink}`; } - + return ( <>
@@ -71,68 +72,100 @@ export default function ConnectionTable() { Linked -

{linkedConnections("valid")?.length}

+

+ {linkedConnections("valid")?.length} +

- - Incomplete Link + + Incomplete Link + -

{linkedConnections("1")?.length}

+

+ {linkedConnections("1")?.length} +

- Relink Needed + + Relink Needed + -

{linkedConnections("2")?.length}

+

+ {linkedConnections("2")?.length} +

-
- -
- {isGenerated ? - - - - - - Share this magic link with your customers - - Once they finish the oAuth flow, a new connection would be enabled. - - -
-
- -
-
- - - -
-
: + + + + + Share this magic link with your customers + + + Once they finish the oAuth flow, a new connection would be + enabled. + + +
+
+ +
+
+ + + +
+ + ) : ( - } - - {ts && } - + )} + {ts && } - ) -} \ No newline at end of file + ); +} diff --git a/apps/webapp/src/components/Connection/CopyLinkInput.tsx b/apps/webapp/src/components/Connection/CopyLinkInput.tsx index 1946bf6b5..511d2b747 100644 --- a/apps/webapp/src/components/Connection/CopyLinkInput.tsx +++ b/apps/webapp/src/components/Connection/CopyLinkInput.tsx @@ -1,23 +1,23 @@ -'use client' +"use client"; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import useMagicLinkStore from '@/state/magicLinkStore'; -import config from '@/lib/config'; -import { useState } from 'react'; -import { LoadingSpinner } from './LoadingSpinner'; -import { toast } from 'sonner'; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import useMagicLinkStore from "@/state/magicLinkStore"; +import config from "@/lib/config"; +import { useState } from "react"; +import { LoadingSpinner } from "./LoadingSpinner"; +import { toast } from "sonner"; const CopyLinkInput = () => { const [copied, setCopied] = useState(false); - const {uniqueLink} = useMagicLinkStore(); + const { uniqueLink } = useMagicLinkStore(); let link: string; - if(config.DISTRIBUTION == 'selfhost' && config.REDIRECT_WEBHOOK_INGRESS) { - link = `${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}&redirectIngressUri=${config.REDIRECT_WEBHOOK_INGRESS}` - }else{ - link = `${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}` + if (config.DISTRIBUTION == "selfhost" && config.REDIRECT_WEBHOOK_INGRESS) { + link = `${config.WEBAPP_URL}/magic-link?uniqueLink=${uniqueLink}&redirectIngressUri=${config.REDIRECT_WEBHOOK_INGRESS}`; + } else { + link = `${config.WEBAPP_URL}/magic-link?uniqueLink=${uniqueLink}`; } const handleCopy = async () => { @@ -28,40 +28,56 @@ const CopyLinkInput = () => { label: "Close", onClick: () => console.log("Close"), }, - }) + }); setCopied(true); setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds } catch (err) { - console.error('Failed to copy: ', err); + console.error("Failed to copy: ", err); } }; return ( <> - {uniqueLink !== 'https://' ? - <> - - - : -
- + {uniqueLink !== "https://" ? ( + <> + + + + ) : ( +
+
- } + )} ); }; diff --git a/apps/webapp/src/components/magic-link/custom-modal.tsx b/apps/webapp/src/components/magic-link/custom-modal.tsx new file mode 100644 index 000000000..8a4868c3d --- /dev/null +++ b/apps/webapp/src/components/magic-link/custom-modal.tsx @@ -0,0 +1,43 @@ +"use client"; +import React, { useState } from "react"; +import { X } from "lucide-react"; + +interface ModalProps { + open: boolean; + setOpen: (op: boolean) => void; + children: React.ReactNode; + backgroundClass?: string; + contentClass?: string; +} + +const CustomModal: React.FC = ({ + open, + setOpen, + children, + backgroundClass = "bg-black/20 backdrop-blur ", + contentClass = "", +}) => { + if (!open) return null; + + return ( +
setOpen(false)} + className={` + fixed inset-0 flex justify-center items-center transition-colors + ${backgroundClass} + `} + > + {/* modal */} +
e.stopPropagation()} + className={` + ${contentClass} transition-all + `} + > + {children} +
+
+ ); +}; + +export default CustomModal; diff --git a/apps/webapp/src/hooks/magic-link/useCreateApiKeyConnection.tsx b/apps/webapp/src/hooks/magic-link/useCreateApiKeyConnection.tsx new file mode 100644 index 000000000..fd0c9514e --- /dev/null +++ b/apps/webapp/src/hooks/magic-link/useCreateApiKeyConnection.tsx @@ -0,0 +1,50 @@ +import config from "@/lib/config"; +import { useMutation } from "@tanstack/react-query"; + +interface IGConnectionDto { + query: { + providerName: string; // Name of the API Key provider + vertical: string; // Vertical (Crm, Ticketing, etc) + projectId: string; // Project ID + linkedUserId: string; // Linked User ID + }; + data: { + [key: string]: string; + }; +} + +// Adjusted useCreateApiKey hook to include a promise-returning function +const useCreateApiKeyConnection = () => { + const createApiKeyConnection = async ( + apiKeyConnectionData: IGConnectionDto + ) => { + const response = await fetch( + `${ + config.API_URL + }/connections/basicorapikey/callback?state=${encodeURIComponent( + JSON.stringify(apiKeyConnectionData.query) + )}`, + { + method: "POST", + body: JSON.stringify(apiKeyConnectionData.data), + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "Unknown error occurred"); + } + + return response; + }; + + return useMutation({ + mutationFn: createApiKeyConnection, + onSuccess: () => {}, + }); +}; + +export default useCreateApiKeyConnection; diff --git a/apps/webapp/src/hooks/magic-link/useOAuth.ts b/apps/webapp/src/hooks/magic-link/useOAuth.ts new file mode 100644 index 000000000..9f8ffaf99 --- /dev/null +++ b/apps/webapp/src/hooks/magic-link/useOAuth.ts @@ -0,0 +1,97 @@ +import config from '@/lib/config'; +import { useState, useEffect, useRef } from 'react'; +import { constructAuthUrl } from '@panora/shared/src/test'; + +type UseOAuthProps = { + clientId?: string; + providerName: string; // Name of the OAuth provider + vertical: string; // Vertical (Crm, Ticketing, etc) + returnUrl: string; // Return URL after OAuth flow + projectId: string; // Project ID + linkedUserId: string; // Linked User ID + redirectIngressUri: { + status: boolean; + value: string | null; + }, + onSuccess: () => void; + additionalParams?: {[key: string]: any} +}; + +const useOAuth = ({ providerName, vertical, returnUrl, projectId, linkedUserId, additionalParams, redirectIngressUri, onSuccess }: UseOAuthProps) => { + const [isReady, setIsReady] = useState(false); + const intervalRef = useRef | null>(null); + const authWindowRef = useRef(null); + + useEffect(() => { + // Perform any setup logic here + setTimeout(() => setIsReady(true), 1000); // Simulating async operation + + return () => { + // Cleanup on unmount + clearExistingInterval(false); + if (authWindowRef.current && !authWindowRef.current.closed) { + authWindowRef.current.close(); + } + }; + }, []); + + const clearExistingInterval = (clearAuthWindow: boolean) => { + if (clearAuthWindow && authWindowRef.current && !authWindowRef.current.closed) { + authWindowRef.current.close(); + } + if (intervalRef.current !== null) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + + + const openModal = async (onWindowClose: () => void) => { + const apiUrl = config.API_URL!; + const authUrl = await constructAuthUrl({ + projectId, linkedUserId, providerName, returnUrl, apiUrl , vertical, additionalParams, redirectUriIngress: redirectIngressUri + }); + + if (!authUrl) { + throw new Error("Auth Url is Invalid " + authUrl); + } + + const width = 600, height = 600; + const left = (window.innerWidth - width) / 2; + const top = (window.innerHeight - height) / 2; + const authWindow = window.open(authUrl as string, '_blank', `width=${width},height=${height},top=${top},left=${left}`); + authWindowRef.current = authWindow; + + clearExistingInterval(false); + + const interval = setInterval(() => { + try { + const redirectedURL = authWindow!.location.href; + const urlParams = new URL(redirectedURL).searchParams; + const success = urlParams.get('success'); // Example parameter + if (redirectedURL === returnUrl || success) { + onSuccess(); + clearExistingInterval(true); + } + } catch (e) { + console.error(e) + } + if (!authWindow || authWindow.closed) { + if (onWindowClose) { + onWindowClose(); + } + authWindowRef.current = null; + clearExistingInterval(false); + } + + }, 500); + + intervalRef.current = interval; + + return authWindow; + }; + + return { open: openModal, isReady }; +}; + +export default useOAuth; diff --git a/apps/webapp/src/hooks/magic-link/useProjectConnectors.tsx b/apps/webapp/src/hooks/magic-link/useProjectConnectors.tsx new file mode 100644 index 000000000..4651f5939 --- /dev/null +++ b/apps/webapp/src/hooks/magic-link/useProjectConnectors.tsx @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; +import config from "@/lib/config"; + +const useProjectConnectors = (id: string | null) => { + return useQuery({ + queryKey: ["project-connectors", id], + queryFn: async (): Promise => { + if (!id) { + throw new Error("Project ID is not available"); + } + const response = await fetch( + `${config.API_URL}/project_connectors?projectId=${id}` + ); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }, + enabled: !!id, // Only run the query if id is truthy + retry: false, // Don't retry if the project ID is not available + }); +}; + +export default useProjectConnectors; diff --git a/apps/webapp/src/hooks/magic-link/useUniqueMagicLink.tsx b/apps/webapp/src/hooks/magic-link/useUniqueMagicLink.tsx new file mode 100644 index 000000000..e4394524a --- /dev/null +++ b/apps/webapp/src/hooks/magic-link/useUniqueMagicLink.tsx @@ -0,0 +1,27 @@ +import { useQuery } from "@tanstack/react-query"; +import { invite_links as MagicLink } from "api"; +import config from "@/lib/config"; + +type Mlink = MagicLink & { id_project: string }; + +const useUniqueMagicLink = (id: string | null) => { + return useQuery({ + queryKey: ["magic-link", id], + queryFn: async (): Promise => { + if (!id) { + throw new Error("Magic Link ID is not available"); + } + const response = await fetch( + `${config.API_URL}/magic_links/${id.trim()}` + ); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }, + enabled: !!id && id.trim().length > 0, // Only run the query if id is truthy and not just whitespace + retry: false, // Don't retry if the magic link ID is not available + }); +}; + +export default useUniqueMagicLink; diff --git a/apps/webapp/src/lib/config.ts b/apps/webapp/src/lib/config.ts index 201afaa65..f5c1c0bc8 100644 --- a/apps/webapp/src/lib/config.ts +++ b/apps/webapp/src/lib/config.ts @@ -1,5 +1,6 @@ const config = { API_URL: process.env.NEXT_PUBLIC_BACKEND_DOMAIN, + WEBAPP_URL: process.env.NEXT_PUBLIC_WEBAPP_DOMAIN, MAGIC_LINK_DOMAIN: process.env.NEXT_PUBLIC_MAGIC_LINK_DOMAIN, REDIRECT_WEBHOOK_INGRESS: process.env.NEXT_PUBLIC_REDIRECT_WEBHOOK_INGRESS, POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, From c18dc4f43acef730c92bbad8d53e00148aaf7de7 Mon Sep 17 00:00:00 2001 From: mit-27 Date: Mon, 18 Nov 2024 01:54:59 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=92=9A=20Fix=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webapp/src/app/(Magic-Link)/magic-link/page.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/webapp/src/app/(Magic-Link)/magic-link/page.tsx b/apps/webapp/src/app/(Magic-Link)/magic-link/page.tsx index 80003727a..4b4490d66 100644 --- a/apps/webapp/src/app/(Magic-Link)/magic-link/page.tsx +++ b/apps/webapp/src/app/(Magic-Link)/magic-link/page.tsx @@ -30,6 +30,7 @@ import { categoriesVerticals } from "@panora/shared/src/categories"; import { ArrowLeft, ArrowLeftRight, Search, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import { Suspense } from "react"; interface IBasicAuthFormData { [key: string]: string; @@ -565,7 +566,7 @@ const ProviderModal = () => {
- You've successfully connected your account! + You've successfully connected your account!
}> + + + ); +}; + +export default MagicLinkPage;