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).
+
+
+
+
#### Import a single connector card
You must import both `@panora/shared` (which contains the types) and `@panora/embedded-card-react`.
@@ -34,12 +43,12 @@ Don't forget to import the styles as well `import "@panora/embedded-card-react/d
const MyPage = () => {
return (
-
)
}
@@ -55,10 +64,10 @@ Don't forget to import the styles as well `import "@panora/embedded-card-react/d
const MyPage = () => {
return (
)
}
@@ -67,9 +76,12 @@ Don't forget to import the styles as well `import "@panora/embedded-card-react/d
Congrats ! You should be able to see the displayed connectors !
+
# Step 2 (optional): Select your own connectors
-**insert**video
+
+
+
You have the ability to choose which connectors will be rendered (also applied to magic-link).
diff --git a/docs/recipes/frontend-sdk.mdx b/docs/recipes/frontend-sdk.mdx
new file mode 100644
index 000000000..20e70c604
--- /dev/null
+++ b/docs/recipes/frontend-sdk.mdx
@@ -0,0 +1,57 @@
+---
+title: "Frontend SDK"
+description: ""
+icon: "square-terminal"
+---
+
+
+
+# Step 1: Import the code snippet
+
+
+ ```shell React
+ pnpm i @panora/frontend-sdk
+ ```
+
+
+
+ For the `vertical` prop, learn more about values accepted
+ [here](/glossary/metadata/category).
+
+
+#### Use the sdk
+
+You must import both `@panora/shared` (which contains the types).
+
+
+
+
+
+
+ ```javascript React
+ import { ConnectorCategory } from '@panora/shared'
+ import Panora from '@panora/frontend-sdk'
+
+ const panora = new Panora({ apiKey: 'YOUR_PRIVATE_API_KEY', overrideApiUrl: 'http://localhost:3000' });
+
+ const MyPage = () => {
+ return (
+ // kickstart the connection (OAuth, ApiKey, Basic)
+ panora.connect({
+ providerName: "hubspot",
+ vertical: ConnectorCategory.Crm,
+ linkedUserId: "4c6ca51b-7b23-4e3a-9309-24d2d331a04d",
+ })
+ )
+ }
+ ```
+
+
+By default, for OAuth we use Panora managed OAuth apps but if we dont have one registered OR you want to use your own, check this [guide](/recipes/add-custom-provider-creds) and it will automatically use these custom credentials !
+
+Congrats ! You should be able to connect !
diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json
index 9b3bf9c0a..0eb814b51 100644
--- a/packages/api/swagger/swagger-spec.json
+++ b/packages/api/swagger/swagger-spec.json
@@ -3663,6 +3663,21 @@
]
}
},
+ "/projects/current": {
+ "get": {
+ "operationId": "getProjects",
+ "summary": "Retrieve projects",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "projects"
+ ]
+ }
+ },
"/projects": {
"get": {
"operationId": "getProjects",
diff --git a/packages/api/swagger/swagger-spec.yaml b/packages/api/swagger/swagger-spec.yaml
index 2678190c3..474b4b18e 100644
--- a/packages/api/swagger/swagger-spec.yaml
+++ b/packages/api/swagger/swagger-spec.yaml
@@ -2113,7 +2113,7 @@ paths:
'201':
description: ''
tags: *ref_38
- /projects:
+ /projects/current:
get:
operationId: getProjects
summary: Retrieve projects
@@ -2123,6 +2123,15 @@ paths:
description: ''
tags: &ref_39
- projects
+ /projects:
+ get:
+ operationId: getProjects
+ summary: Retrieve projects
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ tags: *ref_39
post:
operationId: createProject
summary: Create a project
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 38f8f976c..9ea874ff5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -126,7 +126,7 @@ importers:
specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.51.2)
'@panora/shared':
- specifier: ^1.3.0
+ specifier: ^1.4.0
version: link:../../packages/shared
'@panora/typescript-sdk':
specifier: ^1.0.3