diff --git a/apps/client-ts/src/app/b2c/login/page.tsx b/apps/client-ts/src/app/b2c/login/page.tsx index 5784cd977..aeb85e556 100644 --- a/apps/client-ts/src/app/b2c/login/page.tsx +++ b/apps/client-ts/src/app/b2c/login/page.tsx @@ -54,9 +54,11 @@ export default function Page() { {!userInitialized ? (
-
+
+
- +
+ Login Create Account diff --git a/apps/client-ts/src/app/layout.tsx b/apps/client-ts/src/app/layout.tsx index f1ba7bb41..8eb767539 100644 --- a/apps/client-ts/src/app/layout.tsx +++ b/apps/client-ts/src/app/layout.tsx @@ -22,7 +22,7 @@ export default function RootLayout({ diff --git a/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx b/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx index 36528c49d..0cf0c83e7 100644 --- a/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx +++ b/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx @@ -2,6 +2,40 @@ import { ColumnDef } from "@tanstack/react-table"; import { ColumnLU } from "./schema"; import { DataTableColumnHeader } from "@/components/shared/data-table-column-header"; import { Badge } from "@/components/ui/badge"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { toast } from "sonner"; + + +const LinkedUserIdComponent = ({ row } : {row:any}) => { + + const handleCopyLinkedUserId = () => { + navigator.clipboard.writeText(row.getValue("linked_user_id")); + toast.success("LinkedUser ID copied!", { + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }; + + return ( +
+ + + + + {row.getValue("linked_user_id")} + + + +

Copy

+
+
+
+
+ ) +} + export const columns: ColumnDef[] = [ { @@ -9,13 +43,7 @@ export const columns: ColumnDef[] = [ header: ({ column }) => ( ), - cell: ({ row }) =>{ - return ( -
- {row.getValue("linked_user_id")} -
- ) - }, + cell: LinkedUserIdComponent, enableSorting: false, enableHiding: false, }, diff --git a/apps/client-ts/src/components/Nav/main-nav-sm.tsx b/apps/client-ts/src/components/Nav/main-nav-sm.tsx index 05e097ba6..390c13f94 100644 --- a/apps/client-ts/src/components/Nav/main-nav-sm.tsx +++ b/apps/client-ts/src/components/Nav/main-nav-sm.tsx @@ -11,6 +11,13 @@ import { import { MenuIcon } from "lucide-react" import { useState } from "react" import Link from "next/link" +import { useTheme } from 'next-themes'; +import {User,LogOut} from 'lucide-react'; +import { useRouter } from "next/navigation"; +import Cookies from 'js-cookie'; +import useProjectStore from "@/state/projectStore"; +import { useQueryClient } from '@tanstack/react-query'; +import useProfileStore from "@/state/profileStore"; export function SmallNav({ @@ -19,7 +26,12 @@ export function SmallNav({ onLinkClick: (name: string) => void }) { const [selectedItem, setSelectedItem] = useState("dashboard"); + const router = useRouter(); + const { profile, setProfile } = useProfileStore(); + const { setIdProject } = useProjectStore(); + const queryClient = useQueryClient(); const [open, setOpen] = useState(false); + const { theme } = useTheme() const navItemClassName = (itemName: string) => `text-sm border-b font-medium w-full text-left mx-0 py-2 dark:hover:bg-zinc-900 hover:bg-zinc-200 cursor-pointer ${ selectedItem === itemName ? "dark:bg-zinc-800 bg-zinc-200" : "text-muted-foreground" @@ -30,6 +42,15 @@ export function SmallNav({ onLinkClick(name); setOpen(false); } + + const onLogout = () => { + router.push('/b2c/login') + Cookies.remove("access_token") + setProfile(null) + setIdProject("") + queryClient.clear() + } + return (
@@ -39,26 +60,15 @@ export function SmallNav({ - setOpen(false)}> - Panora. + setOpen(false)}> + {theme == "light" ? : } + diff --git a/apps/client-ts/src/components/Nav/main-nav.tsx b/apps/client-ts/src/components/Nav/main-nav.tsx index 0c477c8c7..73bb49dff 100644 --- a/apps/client-ts/src/components/Nav/main-nav.tsx +++ b/apps/client-ts/src/components/Nav/main-nav.tsx @@ -19,7 +19,7 @@ export function MainNav({ }, [pathname]) const navItemClassName = (itemName: string) => - `group flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground cursor-pointer ${ + `group flex items-center rounded-md px-2 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground cursor-pointer ${ selectedItem === itemName ? 'bg-accent' : 'transparent' } transition-colors`; @@ -63,7 +63,7 @@ export function MainNav({ target="_blank" rel="noopener noreferrer" > -

Documentation

+

Docs

diff --git a/apps/client-ts/src/components/Nav/user-nav.tsx b/apps/client-ts/src/components/Nav/user-nav.tsx index e42590d71..386520d9f 100644 --- a/apps/client-ts/src/components/Nav/user-nav.tsx +++ b/apps/client-ts/src/components/Nav/user-nav.tsx @@ -6,25 +6,36 @@ import { import { Button } from "@/components/ui/button" import { DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, + DropdownMenuPortal, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import useProfileStore from "@/state/profileStore"; import { useRouter } from "next/navigation"; import Link from "next/link"; import Cookies from 'js-cookie'; -import useProjectStore from "@/state/projectStore" +import useProjectStore from "@/state/projectStore"; import { useQueryClient } from '@tanstack/react-query'; +import {User,LogOut,SunMoon,Sun,Moon,Monitor} from 'lucide-react'; +import {DotsHorizontalIcon} from '@radix-ui/react-icons'; +import { useTheme } from "next-themes"; + export function UserNav() { const router = useRouter(); const { profile, setProfile } = useProfileStore(); const { setIdProject } = useProjectStore(); const queryClient = useQueryClient(); + const { setTheme,theme } = useTheme(); + // const [currentTheme,SetCurrentTheme] = useState(theme) const onLogout = () => { router.push('/b2c/login') @@ -36,37 +47,79 @@ export function UserNav() { return ( - +

+ {profile ? `${profile.first_name} ${profile.last_name}` : ""} +

+ + +
+ {/* */} - - + + {/*

{profile ? profile.email || profile.first_name : "No profile found"}

-
- +
*/} + {/* */} - Profile + + Profile + + + + + Theme + + + + setTheme("light")} + > + + Light + + setTheme("dark")} + > + + Dark + + setTheme("system")} + > + + System + + + + + {/* Billing Settings */} - + {/* */} onLogout()} > - Log out + + Log Out
diff --git a/apps/client-ts/src/components/RootLayout/index.tsx b/apps/client-ts/src/components/RootLayout/index.tsx index a54968a2b..64f90ff34 100644 --- a/apps/client-ts/src/components/RootLayout/index.tsx +++ b/apps/client-ts/src/components/RootLayout/index.tsx @@ -13,10 +13,15 @@ import { ThemeToggle } from '@/components/Nav/theme-toggle'; import useProjects from '@/hooks/get/useProjects'; import useRefreshAccessTokenMutation from '@/hooks/create/useRefreshAccessToken'; import { useTheme } from 'next-themes'; +import { useState } from "react"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; export const RootLayout = ({children}:{children:React.ReactNode}) => { const router = useRouter() const base = process.env.NEXT_PUBLIC_WEBAPP_DOMAIN; + const [copiesProjectID, SetCopiesProjectID] = useState(false); const {data : projectsData} = useProjects(); const { idProject, setIdProject } = useProjectStore(); const {mutate : refreshAccessToken} = useRefreshAccessTokenMutation() @@ -42,9 +47,23 @@ export const RootLayout = ({children}:{children:React.ReactNode}) => { } }; + const handleCopyRight = () => { + navigator.clipboard.writeText(idProject); + toast.success("Project ID copied!", { + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + SetCopiesProjectID(true); + setTimeout(() => { + SetCopiesProjectID(false); + }, 2000); + }; + return ( <> -
+ {/*
-
+
*/}
+ -
{children}
+
+ +
+
{children}
diff --git a/apps/client-ts/src/components/shared/team-switcher.tsx b/apps/client-ts/src/components/shared/team-switcher.tsx index e1b3826e1..2ec1b460e 100644 --- a/apps/client-ts/src/components/shared/team-switcher.tsx +++ b/apps/client-ts/src/components/shared/team-switcher.tsx @@ -170,7 +170,7 @@ export default function TeamSwitcher({ className ,projects}: TeamSwitcherProps) - + diff --git a/apps/embedded-catalog/react/package.json b/apps/embedded-catalog/react/package.json index 93b967b44..efd8bdcdf 100644 --- a/apps/embedded-catalog/react/package.json +++ b/apps/embedded-catalog/react/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@panora/shared": "workspace:^", + "lucide-react": "^0.344.0", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-query": "^5.12.2", diff --git a/apps/embedded-catalog/react/src/components/Modal.tsx b/apps/embedded-catalog/react/src/components/Modal.tsx new file mode 100644 index 000000000..7b104bc6f --- /dev/null +++ b/apps/embedded-catalog/react/src/components/Modal.tsx @@ -0,0 +1,32 @@ +import React from 'react' + +const Modal = ({open,setOpen,children} : {open:boolean,setOpen: React.Dispatch>,children: React.ReactNode}) => { + return ( +
setOpen(false)} + className={` + fixed inset-0 flex justify-center items-center transition-colors + ${open ? "visible bg-black/20 backdrop-blur" : "invisible"} + `} + > + {/* modal */} +
e.stopPropagation()} + className={` + bg-[#1d1d1d] border-green-900 rounded-xl shadow p-6 transition-all + ${open ? "scale-100 opacity-100" : "scale-125 opacity-0"} + `} + > + {/* */} + {children} +
+
+ ) +} + +export default Modal \ No newline at end of file diff --git a/apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx b/apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx index 16b09ffca..463c7a153 100644 --- a/apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx +++ b/apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx @@ -1,20 +1,21 @@ import {useState,useEffect} from 'react' -import {providersArray, ConnectorCategory, categoryFromSlug, Provider} from '@panora/shared'; +import {providersArray, ConnectorCategory, categoryFromSlug, Provider,CONNECTORS_METADATA} from '@panora/shared'; import useOAuth from '@/hooks/useOAuth'; import useProjectConnectors from '@/hooks/queries/useProjectConnectors'; import { Card } from './ui/card'; import { Button } from './ui/button2' import { ArrowRightIcon } from '@radix-ui/react-icons'; - +import {ArrowLeftRight} from 'lucide-react' +import Modal from './Modal'; +import config from '@/helpers/config'; export interface DynamicCardProp { projectId: string; - returnUrl: string; linkedUserId: string; category?: ConnectorCategory; optionalApiUrl?: string, } -const DynamicCatalog = ({projectId,returnUrl,linkedUserId, category, optionalApiUrl} : DynamicCardProp) => { +const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : DynamicCardProp) => { // by default we render all integrations but if category is provided we filter by category @@ -29,20 +30,30 @@ const DynamicCatalog = ({projectId,returnUrl,linkedUserId, category, optionalApi const [error,setError] = useState(false); const [startFlow, setStartFlow] = useState(false); + const [openSuccessDialog,setOpenSuccessDialog] = useState(false); + const [currentProviderLogoURL,setCurrentProviderLogoURL] = useState('') + const [currentProvider,setCurrentProvider] = useState('') + const returnUrlWithWindow = (typeof window !== 'undefined') + ? window.location.href + : ''; + const [data, setData] = useState([]); const { open, isReady } = useOAuth({ providerName: selectedProvider?.provider!, vertical: selectedProvider?.category! as ConnectorCategory, - returnUrl: returnUrl, + returnUrl: returnUrlWithWindow, projectId: projectId, linkedUserId: linkedUserId, optionalApiUrl: optionalApiUrl, - onSuccess: () => console.log('OAuth successful'), + onSuccess: () => { + console.log('OAuth successful'); + setOpenSuccessDialog(true); + }, }); - const {data: connectorsForProject} = useProjectConnectors(projectId); + const {data: connectorsForProject} = useProjectConnectors(projectId,optionalApiUrl ? optionalApiUrl : config.API_URL!); const onWindowClose = () => { setSelectedProvider({ @@ -65,7 +76,7 @@ const DynamicCatalog = ({projectId,returnUrl,linkedUserId, category, optionalApi provider: '' }); } - }, [startFlow, isReady, open]); + }, [startFlow, isReady]); useEffect(()=>{ const PROVIDERS = !category ? providersArray() : providersArray(category); @@ -88,6 +99,9 @@ const DynamicCatalog = ({projectId,returnUrl,linkedUserId, category, optionalApi const handleStartFlow = (walletName: string, category: string) => { setSelectedProvider({provider: walletName.toLowerCase(), category: category.toLowerCase()}); + const logoPath = CONNECTORS_METADATA[category.toLowerCase()][walletName.toLowerCase()].logoPath; + setCurrentProviderLogoURL(logoPath) + setCurrentProvider(walletName.toLowerCase()) setLoading({status: true, provider: selectedProvider?.provider!}); setStartFlow(true); } @@ -138,6 +152,27 @@ const DynamicCatalog = ({projectId,returnUrl,linkedUserId, category, optionalApi )}) } + + + {/* OAuth Successful Modal */} + +
+
+
+ + + + {selectedProvider?.provider} + +
+ +
Connection Successful!
+ +
The connection with {currentProvider} was successfully established. You can visit the Dashboard and verify the status.
+ +
+
+
) } diff --git a/apps/embedded-catalog/react/src/components/PanoraIntegrationCard.tsx b/apps/embedded-catalog/react/src/components/PanoraIntegrationCard.tsx index 5b385f52f..92e2d4822 100644 --- a/apps/embedded-catalog/react/src/components/PanoraIntegrationCard.tsx +++ b/apps/embedded-catalog/react/src/components/PanoraIntegrationCard.tsx @@ -4,33 +4,47 @@ import { CONNECTORS_METADATA, ConnectorCategory } from '@panora/shared'; import { Button } from './ui/button2'; import { Card } from './ui/card'; import { ArrowRightIcon } from '@radix-ui/react-icons'; +import {ArrowLeftRight} from 'lucide-react' +import Modal from './Modal'; + + export interface ProviderCardProp { name: string; category: ConnectorCategory; projectId: string; - returnUrl: string; linkedUserId: string; optionalApiUrl?: string, } -const PanoraIntegrationCard = ({name, category, projectId, returnUrl, linkedUserId, optionalApiUrl}: ProviderCardProp) => { - const [loading, setLoading] = useState(false) +const PanoraIntegrationCard = ({name, category, projectId, linkedUserId, optionalApiUrl}: ProviderCardProp) => { + const [loading, setLoading] = useState(false); + const [openSuccessDialog,setOpenSuccessDialog] = useState(false); const [startFlow, setStartFlow] = useState(false); + const returnUrlWithWindow = (typeof window !== 'undefined') + ? window.location.href + : ''; + const { open, isReady } = useOAuth({ providerName: name.toLowerCase(), vertical: category.toLowerCase() as ConnectorCategory, - returnUrl: returnUrl, + returnUrl: returnUrlWithWindow, + // returnUrl: window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''), + // returnUrl: returnUrl, projectId: projectId, linkedUserId: linkedUserId, optionalApiUrl: optionalApiUrl, - onSuccess: () => console.log('OAuth successful'), + onSuccess: () => { + console.log('OAuth successful'); + setOpenSuccessDialog(true); + }, }); const onWindowClose = () => { setLoading(false); - return; + setStartFlow(false); + // return; } useEffect(() => { @@ -39,7 +53,7 @@ const PanoraIntegrationCard = ({name, category, projectId, returnUrl, linkedUser } else if (startFlow && !isReady) { setLoading(false); } - }, [startFlow, isReady, open]); + }, [startFlow, isReady]); const handleStartFlow = () => { @@ -78,6 +92,26 @@ const PanoraIntegrationCard = ({name, category, projectId, returnUrl, linkedUser
+ + {/* OAuth Successful Modal */} + +
+
+
+ + + + {name} + +
+ +
Connection Successful!
+ +
The connection with {name} was successfully established. You can visit the Dashboard and verify the status.
+ +
+
+
) }; diff --git a/apps/embedded-catalog/react/src/hooks/queries/useProjectConnectors.tsx b/apps/embedded-catalog/react/src/hooks/queries/useProjectConnectors.tsx index 1949f8162..34de913ae 100644 --- a/apps/embedded-catalog/react/src/hooks/queries/useProjectConnectors.tsx +++ b/apps/embedded-catalog/react/src/hooks/queries/useProjectConnectors.tsx @@ -1,11 +1,11 @@ import { useQuery } from '@tanstack/react-query'; import config from '@/helpers/config'; -const useProjectConnectors = (id: string) => { +const useProjectConnectors = (id: string,API_URL : string) => { return useQuery({ queryKey: ['project-connectors', id], queryFn: async (): Promise => { - const response = await fetch(`${config.API_URL}/project-connectors?projectId=${id}`); + const response = await fetch(`${API_URL}/project-connectors?projectId=${id}`); if (!response.ok) { throw new Error('Network response was not ok'); } diff --git a/apps/embedded-catalog/react/src/hooks/useOAuth.tsx b/apps/embedded-catalog/react/src/hooks/useOAuth.tsx index 6209b069b..dcb00863b 100644 --- a/apps/embedded-catalog/react/src/hooks/useOAuth.tsx +++ b/apps/embedded-catalog/react/src/hooks/useOAuth.tsx @@ -1,11 +1,11 @@ import config from '@/helpers/config'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { ConnectorCategory, constructAuthUrl } from '@panora/shared'; type UseOAuthProps = { clientId?: string; providerName: string; // Name of the OAuth provider - vertical: ConnectorCategory; // Vertical (Crm, Ticketing, etc) + vertical: ConnectorCategory; // Vertical (Crm, Ticketing, etc) returnUrl: string; // Return URL after OAuth flow projectId: string; // Project ID linkedUserId: string; // Linked User ID @@ -15,36 +15,79 @@ type UseOAuthProps = { const useOAuth = ({ providerName, vertical, returnUrl, projectId, linkedUserId, optionalApiUrl, onSuccess }: UseOAuthProps) => { const [isReady, setIsReady] = useState(false); + const intervalRef = useRef | null>(null); + const authWindowRef = useRef(null); + const clearExistingInterval = (clearAuthWindow : boolean) => { + if (clearAuthWindow && authWindowRef.current && !authWindowRef.current.closed) { + authWindowRef.current.close(); + } + if (intervalRef.current !== null) { + clearInterval(intervalRef.current); + intervalRef.current = 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 openModal = async (onWindowClose: () => void) => { - const apiUrl = optionalApiUrl? optionalApiUrl : config.API_URL!; + const apiUrl = optionalApiUrl ? optionalApiUrl : config.API_URL!; const authUrl = await constructAuthUrl({ projectId, linkedUserId, providerName, returnUrl, apiUrl, vertical }); - if(!authUrl) { - throw new Error("Auth Url is Invalid "+ authUrl) + + 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, 'OAuth', `width=${width},height=${height},top=${top},left=${left}`); - + authWindowRef.current = authWindow; + + clearExistingInterval(false); + const interval = setInterval(() => { - if (authWindow!.closed) { - clearInterval(interval); + try{ + const redirectedURL = authWindow!.location.href; + // const redirectedURL = authWindow!.location.protocol + '//' + authWindow!.location.hostname + (authWindow!.location.port ? ':' + authWindow!.location.port : ''); + if(redirectedURL===returnUrl) + { + onSuccess(); + clearExistingInterval(true); + } + + } catch(e) + { + console.log(e) + } + if (!authWindow || authWindow.closed) { if (onWindowClose) { onWindowClose(); } - onSuccess(); + authWindowRef.current = null; + console.log("Clearing direct close interval") + clearExistingInterval(false); } + }, 500); + intervalRef.current = interval; + return authWindow; }; diff --git a/apps/embedded-catalog/react/src/index.tsx b/apps/embedded-catalog/react/src/index.tsx index 225dfd62c..74f1fb936 100644 --- a/apps/embedded-catalog/react/src/index.tsx +++ b/apps/embedded-catalog/react/src/index.tsx @@ -4,20 +4,20 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import PanoraDynamicCatalog, { DynamicCardProp } from "./components/PanoraDynamicCatalog"; -const PanoraProviderCard = ({name, category, projectId, returnUrl, linkedUserId, optionalApiUrl}: ProviderCardProp) => { +const PanoraProviderCard = ({name, category, projectId, linkedUserId, optionalApiUrl}: ProviderCardProp) => { const queryClient = new QueryClient(); return ( - + ) } -const PanoraDynamicCatalogCard = ({projectId, returnUrl, linkedUserId, category, optionalApiUrl} : DynamicCardProp) => { +const PanoraDynamicCatalogCard = ({projectId, linkedUserId, category, optionalApiUrl} : DynamicCardProp) => { const queryClient = new QueryClient(); return ( - + ) diff --git a/apps/magic-link/src/App.tsx b/apps/magic-link/src/App.tsx index e815e5f7a..a8b5b1a77 100644 --- a/apps/magic-link/src/App.tsx +++ b/apps/magic-link/src/App.tsx @@ -5,7 +5,7 @@ import { ThemeProvider } from "@/components/theme-provider" function App() { return ( -
+
diff --git a/apps/magic-link/src/components/Modal.tsx b/apps/magic-link/src/components/Modal.tsx new file mode 100644 index 000000000..348b83da1 --- /dev/null +++ b/apps/magic-link/src/components/Modal.tsx @@ -0,0 +1,34 @@ +"use client" +import React, { useState } from 'react' +import {PartyPopper, Unplug,X} from 'lucide-react' + +const Modal = ({open,setOpen,children} : {open:boolean,setOpen: (op : boolean) => void,children: React.ReactNode}) => { + return ( +
setOpen(false)} + className={` + fixed inset-0 flex justify-center items-center transition-colors + ${open ? "visible bg-black/20 backdrop-blur" : "invisible"} + `} + > + {/* modal */} +
e.stopPropagation()} + className={` + bg-[#1d1d1d] border-green-900 rounded-xl shadow p-6 transition-all + ${open ? "scale-100 opacity-100" : "scale-125 opacity-0"} + `} + > + {/* */} + {children} +
+
+ ) +} + +export default Modal \ No newline at end of file diff --git a/apps/magic-link/src/hooks/useOAuth.ts b/apps/magic-link/src/hooks/useOAuth.ts index ef590fc9c..43a1281a6 100644 --- a/apps/magic-link/src/hooks/useOAuth.ts +++ b/apps/magic-link/src/hooks/useOAuth.ts @@ -1,55 +1,89 @@ import config from '@/helpers/config'; -import { useState, useEffect } from 'react'; +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: string; // Vertical (Crm, Ticketing, etc) returnUrl: string; // Return URL after OAuth flow projectId: string; // Project ID linkedUserId: string; // Linked User ID + optionalApiUrl?: string; // URL of the User's Server onSuccess: () => void; }; -const useOAuth = ({ providerName, vertical, returnUrl, projectId, linkedUserId, onSuccess }: UseOAuthProps) => { +const useOAuth = ({ providerName, vertical, returnUrl, projectId, linkedUserId, optionalApiUrl, 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) => { - try { - const apiUrl = config.API_URL; - const authUrl = await constructAuthUrl({ - projectId, linkedUserId, providerName, returnUrl, apiUrl, vertical - }); - 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, 'OAuth', `width=${width},height=${height},top=${top},left=${left}`); - - const interval = setInterval(() => { - if (authWindow!.closed) { - clearInterval(interval); - if (onWindowClose) { - onWindowClose(); - } + const apiUrl = optionalApiUrl ? optionalApiUrl : config.API_URL!; + const authUrl = await constructAuthUrl({ + projectId, linkedUserId, providerName, returnUrl, apiUrl, vertical + }); + + 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, 'OAuth', `width=${width},height=${height},top=${top},left=${left}`); + authWindowRef.current = authWindow; + + clearExistingInterval(false); + + const interval = setInterval(() => { + try { + const redirectedURL = authWindow!.location.href; + // const redirectedURL = authWindow!.location.protocol + '//' + authWindow!.location.hostname + (authWindow!.location.port ? ':' + authWindow!.location.port : ''); + if (redirectedURL === returnUrl) { onSuccess(); + clearExistingInterval(true); } - }, 500); - return authWindow; - } catch (error) { - console.error('Failed to open OAuth window', error); - onWindowClose(); // Reset the loading state - } + } 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 }; diff --git a/apps/magic-link/src/lib/ProviderModal.tsx b/apps/magic-link/src/lib/ProviderModal.tsx index 98befc4e6..0a3d18211 100644 --- a/apps/magic-link/src/lib/ProviderModal.tsx +++ b/apps/magic-link/src/lib/ProviderModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import useOAuth from '@/hooks/useOAuth'; -import { findProviderByName, providersArray, categoryFromSlug, Provider } from '@panora/shared/src'; +import { findProviderByName, providersArray, categoryFromSlug, Provider,CONNECTORS_METADATA } from '@panora/shared/src'; import { categoriesVerticals } from '@panora/shared/src/categories'; import useLinkedUser from '@/hooks/queries/useLinkedUser'; import useUniqueMagicLink from '@/hooks/queries/useUniqueMagicLink'; @@ -11,26 +11,28 @@ import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; import useProjectConnectors from '@/hooks/queries/useProjectConnectors'; +import {ArrowLeftRight} from 'lucide-react' +import Modal from '@/components/Modal'; +import logo from '../../public/assets/logo.png' - -const LoadingOverlay = ({ providerName }: { providerName: string }) => { - const provider = findProviderByName(providerName); - return ( -
-
-
- {provider!.name} -
+// const LoadingOverlay = ({ providerName }: { providerName: string }) => { +// const provider = findProviderByName(providerName); +// return ( +//
+//
+//
+// {provider!.name} +//
-

Continue in {provider!.name}

-

Accepting oAuth access to Panora

-
+//

Continue in {provider!.name}

+//

Accepting oAuth access to Panora

+//
-
-
-
- ); -}; +//
+//
+//
+// ); +// }; const ProviderModal = () => { const [selectedCategory, setSelectedCategory] = useState("All"); @@ -42,12 +44,19 @@ const ProviderModal = () => { const [preStartFlow, setPreStartFlow] = useState(false); const [projectId, setProjectId] = useState(""); const [data, setData] = useState([]); + const [errorResponse,setErrorResponse] = useState<{ + errorPresent: boolean; errorMessage : string + }>({errorPresent:false,errorMessage:''}) const [loading, setLoading] = useState<{ status: boolean; provider: string }>({status: false, provider: ''}); const [uniqueMagicLinkId, setUniqueMagicLinkId] = useState(''); + const [openSuccessDialog,setOpenSuccessDialog] = useState(false); + const [currentProviderLogoURL,setCurrentProviderLogoURL] = useState('') + const [currentProvider,setCurrentProvider] = useState('') + const {data: magicLink} = useUniqueMagicLink(uniqueMagicLinkId); const {data: connectorsForProject} = useProjectConnectors(projectId); @@ -90,17 +99,24 @@ const ProviderModal = () => { const { open, isReady } = useOAuth({ providerName: selectedProvider?.provider!, vertical: selectedProvider?.category!, - returnUrl: "https://google.com", + // Providing current URL to avoid cross origin error + returnUrl: window.location.href, + // returnUrl: window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''), projectId: projectId, linkedUserId: magicLink?.id_linked_user as string, - onSuccess: () => console.log('OAuth successful'), + onSuccess: () => { + console.log('OAuth successful'); + setOpenSuccessDialog(true); + }, }); const onWindowClose = () => { + setSelectedProvider({ provider: '', category: '' - }); + }); + setLoading({ status: false, provider: '' @@ -111,19 +127,35 @@ const ProviderModal = () => { useEffect(() => { if (startFlow && isReady) { - open(onWindowClose); + setErrorResponse({errorPresent:false,errorMessage:''}) + + open(onWindowClose) + .catch((error : Error) => { + console.log("Error in use0Auth : ",error.message); + setLoading({ + status: false, + provider: '' + }); + setErrorResponse({errorPresent:true,errorMessage:error.message}) + setStartFlow(false); + setPreStartFlow(false); + }); + } else if (startFlow && !isReady) { setLoading({ status: false, provider: '' }); } - }, [startFlow, isReady, open]); + }, [startFlow, isReady]); const handleWalletClick = (walletName: string, category: string) => { setSelectedProvider({provider: walletName.toLowerCase(), category: category.toLowerCase()}); + const logoPath = CONNECTORS_METADATA[category.toLowerCase()][walletName.toLowerCase()].logoPath; + setCurrentProviderLogoURL(logoPath); + setCurrentProvider(walletName.toLowerCase()) setPreStartFlow(true); }; @@ -141,6 +173,15 @@ const ProviderModal = () => { setSelectedCategory(category); }; + const CloseSuccessDialog = (close : boolean) => { + if(!close) + { + setCurrentProvider(''); + setCurrentProviderLogoURL('') + setOpenSuccessDialog(close); + } + } + function transformConnectorsStatus(connectors : {[key: string]: boolean}): { 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()); @@ -158,6 +199,7 @@ const ProviderModal = () => { return ( + <> Connect to your software @@ -205,10 +247,35 @@ const ProviderModal = () => {
- - {loading.status ? : } + + + {loading.status ? : } + {errorResponse.errorPresent ?

{errorResponse.errorMessage}

: (<>)} + + {/* */}
+ + {/* OAuth Successful Modal */} + +
+
+
+ + + + {selectedProvider?.provider} + +
+ +
Connection Successful!
+ +
The connection with {currentProvider} was successfully established. You can visit the Dashboard and verify the status.
+ +
+
+
+ ); }; diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 7260a3aab..3eeb4cba4 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -154,7 +154,6 @@ export class ConnectionsController { @Get() async getConnections(@Request() req: any) { const { id_project } = req.user; - console.log('Req data is:', req.user); return await this.prisma.connections.findMany({ where: { id_project: id_project, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9515fb60a..cf5aa1096 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,9 @@ importers: clsx: specifier: ^2.1.0 version: 2.1.0 + lucide-react: + specifier: ^0.344.0 + version: 0.344.0(react@18.2.0) react-loader-spinner: specifier: ^5.4.5 version: 5.5.0(@babel/core@7.24.4)(react-dom@18.2.0)(react@18.2.0)