diff --git a/.changeset/rare-garlics-flow.md b/.changeset/rare-garlics-flow.md new file mode 100644 index 000000000..4cddde3f8 --- /dev/null +++ b/.changeset/rare-garlics-flow.md @@ -0,0 +1,6 @@ +--- +"@panora/embedded-card-react": patch +"@panora/frontend-sdk": patch +--- + +Readme patch diff --git a/apps/embedded-catalog/react/README.md b/apps/embedded-catalog/react/README.md index c1e8f2e20..1940a6e64 100644 --- a/apps/embedded-catalog/react/README.md +++ b/apps/embedded-catalog/react/README.md @@ -21,36 +21,51 @@ or yarn add @panora/embedded-card-react ``` -## Import the component +## Import the components -```bash -# Import the css file +```ts import "@panora/embedded-card-react/dist/index.css"; -import PanoraProviderCard from "@panora/embedded-card-react"; +import { PanoraDynamicCatalogCard, PanoraProviderCard } from '@panora/embedded-card-react'; ``` ## Use the component - The `optionalApiUrl` is an optional prop to use the component with the self-hosted version of Panora. -```bash +```ts + + ``` ```ts -These are the types needed for the component. +These are the types needed for the components. + +The `` takes this props type: interface ProviderCardProp { name: string; projectId: string; - returnUrl: string; - linkedUserIdOrRemoteUserInfo: string; + linkedUserId: string; +} + +The `` takes this props type: + +interface DynamicCardProp { + projectId: string; + linkedUserId: string; + category?: ConnectorCategory; + optionalApiUrl?: string, } ``` diff --git a/apps/frontend-sdk/README.md b/apps/frontend-sdk/README.md new file mode 100644 index 000000000..e51b3c32f --- /dev/null +++ b/apps/frontend-sdk/README.md @@ -0,0 +1,75 @@ + +## Frontend SDK (React) + +It is a React component aimed to be used in any of your pages so end-users can connect their 3rd parties in 1-click! + +## Installation + +```bash +npm i @panora/frontend-sdk +``` + +or + +```bash +pnpm i @panora/frontend-sdk +``` + +or + +```bash +yarn add @panora/frontend-sdk +``` + +## Use the component + +```ts + import { ConnectorCategory } from '@panora/shared' + import Panora from '@panora/frontend-sdk' + + const panora = new Panora({ apiKey: 'YOUR_PRIVATE_API_KEY' }); + + // kickstart the connection (OAuth, ApiKey, Basic) + panora.connect({ + providerName: "hubspot", + vertical: ConnectorCategory.Crm, + linkedUserId: "4c6ca51b-7b23-4e3a-9309-24d2d331a04d", + }) +``` + +```ts +The Panora SDK must be instantiated with this type: + +interface PanoraConfig { + apiKey: string; + overrideApiUrl: string; + // Optional (only if you are in selfhost mode and want to use localhost:3000), by default: api.panora.dev +} + +The .connect() function takes this type: + +interface ConnectOptions { + providerName: string; + vertical: ConnectorCategory; // Must be imported from @panora/shared + linkedUserId: string; // You can copy it from your Panora dahsbord under /configuration tab + credentials?: Credentials; // Optional if you try to use OAuth + options?: { + onSuccess?: () => void; + onError?: (error: Error) => void; + overrideReturnUrl?: string; + } +} + +By default, for OAuth we use Panora managed OAuth apps but if we dont have one registered OR you want to use your own, you must register that under /configuration tab from the webapp and it will automatically use these custom credentials ! + +interface Credentials { + username?: string; // Used for Basic Auth + password?: string; // Used for Basic Auth + apiKey?: string; // Used for Api Key Auth +} + +For Basic Auth some providers may only ask for username or password. + +In this case just specify either password or username depending on the 3rd party reference. + +``` diff --git a/apps/frontend-sdk/src/index.ts b/apps/frontend-sdk/src/index.ts index 6a8256af4..86fd60675 100644 --- a/apps/frontend-sdk/src/index.ts +++ b/apps/frontend-sdk/src/index.ts @@ -17,7 +17,7 @@ interface ConnectOptions { vertical: ConnectorCategory; linkedUserId: string; credentials?: Credentials; - options: { + options?: { onSuccess?: () => void; onError?: (error: Error) => void; overrideReturnUrl?: string; @@ -65,7 +65,7 @@ class Panora { } async connect(options: ConnectOptions): Promise { - const { providerName, vertical, linkedUserId, credentials, options: {onSuccess, onError, overrideReturnUrl} } = options; + const { providerName, vertical, linkedUserId, credentials, options: {onSuccess, onError, overrideReturnUrl} = {} } = options; try { const projectId = await this.fetchProjectId(); diff --git a/apps/magic-link/src/hooks/queries/useProjectConnectors.tsx b/apps/magic-link/src/hooks/queries/useProjectConnectors.tsx index 1949f8162..51d9d83a9 100644 --- a/apps/magic-link/src/hooks/queries/useProjectConnectors.tsx +++ b/apps/magic-link/src/hooks/queries/useProjectConnectors.tsx @@ -1,16 +1,22 @@ import { useQuery } from '@tanstack/react-query'; import config from '@/helpers/config'; -const useProjectConnectors = (id: string) => { +const useProjectConnectors = (id: string | null) => { return useQuery({ - queryKey: ['project-connectors', id], + 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(); - } + 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; + +export default useProjectConnectors; \ No newline at end of file diff --git a/apps/magic-link/src/hooks/queries/useUniqueMagicLink.tsx b/apps/magic-link/src/hooks/queries/useUniqueMagicLink.tsx index 61357c2ec..edf98ad60 100644 --- a/apps/magic-link/src/hooks/queries/useUniqueMagicLink.tsx +++ b/apps/magic-link/src/hooks/queries/useUniqueMagicLink.tsx @@ -4,16 +4,22 @@ import config from '@/helpers/config'; type Mlink = MagicLink & {id_project: string} -const useUniqueMagicLink = (id: string) => { - return useQuery({ - queryKey: ['magic-link', id], +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/single?id=${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; + +export default useUniqueMagicLink; \ No newline at end of file diff --git a/apps/magic-link/src/lib/ProviderModal.tsx b/apps/magic-link/src/lib/ProviderModal.tsx index 5bb15a84c..5f24cfb63 100644 --- a/apps/magic-link/src/lib/ProviderModal.tsx +++ b/apps/magic-link/src/lib/ProviderModal.tsx @@ -50,6 +50,7 @@ const ProviderModal = () => { const [openApiKeyDialog,setOpenApiKeyDialog] = 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:''}) @@ -58,14 +59,14 @@ const ProviderModal = () => { status: boolean; provider: string }>({status: false, provider: ''}); - const [uniqueMagicLinkId, setUniqueMagicLinkId] = useState(''); + const [uniqueMagicLinkId, setUniqueMagicLinkId] = useState(null); const [openSuccessDialog,setOpenSuccessDialog] = useState(false); const [currentProviderLogoURL,setCurrentProviderLogoURL] = useState('') const [currentProvider,setCurrentProvider] = useState('') const {mutate : createApiKeyConnection} = useCreateApiKeyConnection(); const {data: magicLink} = useUniqueMagicLink(uniqueMagicLinkId); - const {data: connectorsForProject} = useProjectConnectors(projectId); + const {data: connectorsForProject} = useProjectConnectors(isProjectIdReady ? projectId : null); // const form = useForm>({ // resolver: zodResolver(formSchema), @@ -88,28 +89,28 @@ const ProviderModal = () => { useEffect(() => { if (magicLink) { setProjectId(magicLink?.id_project); + setIsProjectIdReady(true); } }, [magicLink]); useEffect(()=>{ - 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; - } - - if(connectorsForProject) { - setData(getConnectorsToDisplay()) + 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]) + }, [connectorsForProject, selectedCategory, isProjectIdReady]) const { open, isReady } = useOAuth({ providerName: selectedProvider?.provider!, diff --git a/apps/webapp/src/components/shared/data-table-row-actions.tsx b/apps/webapp/src/components/shared/data-table-row-actions.tsx index 00121144f..7cd1e8ffa 100644 --- a/apps/webapp/src/components/shared/data-table-row-actions.tsx +++ b/apps/webapp/src/components/shared/data-table-row-actions.tsx @@ -29,6 +29,7 @@ export function DataTableRowActions({ const {deleteApiKeyPromise} = useDeleteApiKey(); const {deleteWebhookPromise} = useDeleteWebhook(); + const queryClient = useQueryClient(); const handleDeletion = () => { diff --git a/docs/images/custom-connectors-widget.png b/docs/images/custom-connectors-widget.png new file mode 100644 index 000000000..6b1a6ddf4 Binary files /dev/null and b/docs/images/custom-connectors-widget.png differ diff --git a/docs/images/embed-video.mp4 b/docs/images/embed-video.mp4 new file mode 100644 index 000000000..e03fb18d9 Binary files /dev/null and b/docs/images/embed-video.mp4 differ diff --git a/docs/images/frontend-sdk-video.mp4 b/docs/images/frontend-sdk-video.mp4 new file mode 100644 index 000000000..b7929ba51 Binary files /dev/null and b/docs/images/frontend-sdk-video.mp4 differ diff --git a/docs/mint.json b/docs/mint.json index cf7bce06d..3d277cfb0 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -414,6 +414,7 @@ { "group": "Recipes", "pages": [ + "recipes/frontend-sdk", "recipes/embed-catalog", "recipes/import-existing-users", "recipes/catch-connection-token", @@ -433,7 +434,7 @@ "group": "Devtools", "pages": [ { - "group": "SDKs", + "group": "Backend SDKs", "pages": ["backend-sdk/typescript", "backend-sdk/python"] } ] diff --git a/docs/recipes/embed-catalog.mdx b/docs/recipes/embed-catalog.mdx index 5fd06fc97..9771ec3dc 100644 --- a/docs/recipes/embed-catalog.mdx +++ b/docs/recipes/embed-catalog.mdx @@ -10,7 +10,7 @@ icon: "square-terminal" ```shell React - npm i @panora/embedded-card-react + pnpm i @panora/embedded-card-react ``` @@ -20,6 +20,15 @@ icon: "square-terminal" [here](/glossary/metadata/category). + +