Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fe072c6
Fix hydration error in CodeBlock and fix ESLint issues in ConnectSDKDemo
dannyroosevelt Apr 23, 2025
6dc6146
Fixing some linting issues
dannyroosevelt Apr 23, 2025
0355dbd
Update pnpm-lock.yaml
dannyroosevelt Apr 23, 2025
bdbd4a6
Integrate Connect demo components into the getting started sections
dannyroosevelt Apr 23, 2025
c763043
Clean up Connect demo layout in quickstart guide
dannyroosevelt Apr 23, 2025
7629ea6
Split Connect demo into separate step components
dannyroosevelt Apr 23, 2025
7494f47
Create global Connect context to properly separate demo steps
dannyroosevelt Apr 23, 2025
11ea367
Consolidate demo components for better maintainability
dannyroosevelt Apr 23, 2025
d7c9c58
Centralize code snippets in dedicated file to reduce duplication
dannyroosevelt Apr 23, 2025
44efcc1
Remove redundant Connect demo components
dannyroosevelt Apr 23, 2025
0b31b6f
Working well
dannyroosevelt Apr 23, 2025
8b08d2d
connect link handling, more ui polish
dannyroosevelt Apr 23, 2025
a5f79f3
Showing account info on successful connetion
dannyroosevelt Apr 23, 2025
b156115
Restricted allowed origins for API calls
dannyroosevelt Apr 23, 2025
67db40f
Cleaning up code
dannyroosevelt Apr 23, 2025
6186932
Merge branch 'master' into danny/add-connect-sdk-to-docs
dannyroosevelt Apr 23, 2025
fd8b9ef
Adding .env.example
dannyroosevelt Apr 23, 2025
9d365c8
Update pnpm-lock.yaml
dannyroosevelt Apr 23, 2025
df2a700
Linting
dannyroosevelt Apr 23, 2025
e80ca0b
Allowing Vercel preview URLs
dannyroosevelt Apr 23, 2025
50356d8
Code cleanup
dannyroosevelt Apr 23, 2025
27ef904
Linting
dannyroosevelt Apr 23, 2025
21afe03
Update ConnectLinkDemo.jsx
dannyroosevelt Apr 23, 2025
acaac74
Removing references to nextjs example app
dannyroosevelt Apr 23, 2025
064a246
Update GlobalConnectProvider.jsx
dannyroosevelt Apr 23, 2025
8be5e39
Dark mode styling
dannyroosevelt Apr 24, 2025
9434f9a
Update ConnectLinkDemo.jsx
dannyroosevelt Apr 24, 2025
8ca823a
Improve UUID generation
dannyroosevelt Apr 24, 2025
fc87685
Merge branch 'master' into danny/add-connect-sdk-to-docs
dannyroosevelt Apr 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs-v2/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Connect demo API configuration
PIPEDREAM_CLIENT_ID=your_client_id
PIPEDREAM_CLIENT_SECRET=your_client_secret
PIPEDREAM_PROJECT_ID=your_project_id
PIPEDREAM_PROJECT_ENVIRONMENT=development

# Additional redirect URIs for the Connect demo (optional)
PIPEDREAM_CONNECT_TOKEN_WEBHOOK_URI=
PIPEDREAM_CONNECT_SUCCESS_REDIRECT_URI=
PIPEDREAM_CONNECT_ERROR_REDIRECT_URI=

# Comma-separated list of additional allowed origins (optional)
# ALLOWED_ORIGINS=https://your-custom-domain.com,https://another-domain.com
89 changes: 89 additions & 0 deletions docs-v2/components/AccountConnectionDemo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"use client";

import { useGlobalConnect } from "./GlobalConnectProvider";
import CodeBlock from "./CodeBlock";
import { styles } from "../utils/componentStyles";

export default function AccountConnectionDemo() {
const {
appSlug,
setAppSlug,
tokenData,
getClientCodeSnippet,
connectAccount,
connectedAccount,
error,
} = useGlobalConnect();

return (
<div className={styles.container}>
<div className={styles.header}>
Connect an account from your frontend
</div>
<div className="p-4">
<div className="mb-4">
<label className="flex items-center mb-4">
<span className={styles.label}>App to connect:</span>
<select
value={appSlug}
onChange={(e) => setAppSlug(e.target.value)}
className={styles.select}
>
<option value="google_sheets">Google Sheets</option>
<option value="github">GitHub</option>
<option value="notion">Notion</option>
<option value="gmail">Gmail</option>
<option value="openai">OpenAI</option>
</select>
</label>
Comment on lines +32 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Default appSlug ("slack") is missing from the <select> list

GlobalConnectProvider initialises appSlug with "slack", but the option is not rendered here.
On first render the <select> will show a blank value, forcing the user to re-select an app.

               <select
                 value={appSlug}
                 onChange={(e) => setAppSlug(e.target.value)}
                 className="ml-2 p-1 border rounded text-sm"
               >
+                <option value="slack">Slack</option>
                 <option value="google_sheets">Google Sheets</option>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<option value="google_sheets">Google Sheets</option>
<option value="github">GitHub</option>
<option value="notion">Notion</option>
<option value="gmail">Gmail</option>
<option value="openai">OpenAI</option>
</select>
</label>
<select
value={appSlug}
onChange={(e) => setAppSlug(e.target.value)}
className="ml-2 p-1 border rounded text-sm"
>
+ <option value="slack">Slack</option>
<option value="google_sheets">Google Sheets</option>
<option value="github">GitHub</option>
<option value="notion">Notion</option>
<option value="gmail">Gmail</option>
<option value="openai">OpenAI</option>
</select>


<div className="mb-4">
<div className="border border-blue-100 rounded-lg overflow-hidden">
<CodeBlock code={getClientCodeSnippet()} language="javascript" />
</div>
</div>
</div>

<div className="mt-4 mb-2">
<button
onClick={connectAccount}
disabled={!tokenData}
className={styles.primaryButton}
>
Connect Account
</button>
{!tokenData && <p className={`mt-2 ${styles.text.muted}`}><a href="/docs/connect/managed-auth/quickstart/#generate-a-short-lived-token" className="font-semibold underline underline-offset-4 hover:decoration-2 decoration-brand/50">Generate a token above</a> in order to test the account connection flow</p>}
</div>

{error && (
<div className={styles.statusBox.error}>
<div className="font-medium text-sm">Error</div>
<div className="mt-1 text-sm">{error}</div>
</div>
)}

{connectedAccount && (
<div className={styles.statusBox.success}>
<div className="font-medium text-sm">Successfully connected your {appSlug} account!</div>
<div className="mt-4 text-sm">
{connectedAccount.loading
? (
<div>Loading account details...</div>
)
: (
<>
{connectedAccount.name
? (
<div>Account info: <span className="font-medium">{connectedAccount.name}</span></div>
)
: null}
<div>Account ID: <span className="font-medium">{connectedAccount.id}</span></div>
</>
)}
</div>
</div>
)}
</div>
</div>
);
}
109 changes: 109 additions & 0 deletions docs-v2/components/CodeBlock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

import {
useState, useEffect,
} from "react";
// We don't need the default Prism CSS as we're using our custom CSS
// import "prismjs/themes/prism.css";

// We'll dynamically import Prism on the client side only
let Prism;

export default function CodeBlock({
code, language = "javascript", className = "",
}) {
const [
copied,
setCopied,
] = useState(false);
const [
highlightedCode,
setHighlightedCode,
] = useState(code);
const [
isClient,
setIsClient,
] = useState(false);

// Load Prism and highlight code on client-side only
useEffect(() => {
setIsClient(true);

const loadPrism = async () => {
Prism = (await import("prismjs")).default;

// Use manual mode so we can control highlighting
Prism.manual = true;

// Import language definitions dynamically
if (!Prism.languages.javascript) {
await import("prismjs/components/prism-javascript");
}

if (!Prism.languages.json && language === "json") {
await import("prismjs/components/prism-json");
}

// Apply syntax highlighting
try {
if (Prism.languages[language]) {
const highlighted = Prism.highlight(code, Prism.languages[language], language);
setHighlightedCode(highlighted);
}
} catch (error) {
console.error("Prism highlighting error:", error);
}
};

loadPrism();
}, [
code,
language,
]);

const copyToClipboard = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
Comment on lines +64 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for clipboard operations.

The clipboard API might fail in certain browsers or contexts, but there's no error handling in the current implementation.

const copyToClipboard = () => {
-  navigator.clipboard.writeText(code);
-  setCopied(true);
-  setTimeout(() => setCopied(false), 2000);
+  navigator.clipboard.writeText(code)
+    .then(() => {
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    })
+    .catch(err => {
+      console.error("Failed to copy code:", err);
+      // Optionally show a user-friendly error message
+    });
};

This handles potential errors and makes the component more robust.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const copyToClipboard = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const copyToClipboard = () => {
- navigator.clipboard.writeText(code);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ navigator.clipboard.writeText(code)
+ .then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ })
+ .catch(err => {
+ console.error("Failed to copy code:", err);
+ // Optionally show a user-friendly error message
+ });
};


return (
<div className={`relative group ${className}`}>
<pre className="overflow-x-auto rounded-lg font-medium border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-4 text-[13px] leading-relaxed mb-0">
<div className="absolute top-2 right-2 z-10">
<button
onClick={copyToClipboard}
className="opacity-0 group-hover:opacity-100 transition-opacity h-8 w-8 rounded-md bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-300 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label={copied
? "Copied"
: "Copy code"}
>
{copied
? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 13l4 4L19 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 16c0 1.886 0 2.828.586 3.414C9.172 20 10.114 20 12 20h4c1.886 0 2.828 0 3.414-.586C20 18.828 20 17.886 20 16v-4c0-1.886 0-2.828-.586-3.414C18.828 8 17.886 8 16 8m-8 8h4c1.886 0 2.828 0 3.414-.586C16 14.828 16 13.886 16 12V8m-8 8c-1.886 0-2.828 0-3.414-.586C4 14.828 4 13.886 4 12V8c0-1.886 0-2.828.586-3.414C5.172 4 6.114 4 8 4h4c1.886 0 2.828 0 3.414.586C16 5.172 16 6.114 16 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</button>
</div>
{isClient
? (
<code
className={`language-${language} text-gray-800 dark:text-gray-200 [text-shadow:none]`}
dangerouslySetInnerHTML={{
__html: highlightedCode,
}}
Comment on lines +98 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider XSS protection when using dangerouslySetInnerHTML.

While necessary for syntax highlighting, using dangerouslySetInnerHTML can expose users to XSS attacks if the input isn't properly sanitized.

Ensure the code prop is properly sanitized before highlighting or implement a safer alternative for rendering highlighted code. Consider using DOMPurify:


🌐 Web query:

How to safely use dangerouslySetInnerHTML with PrismJS in React?

💡 Result:

Using dangerouslySetInnerHTML with PrismJS in React Safely

When combining PrismJS for syntax highlighting with React components, you'll often need to use dangerouslySetInnerHTML to render the highlighted code. However, this approach requires careful implementation to avoid security vulnerabilities. Here's a comprehensive guide on how to safely implement this combination.

Understanding the Risks

The dangerouslySetInnerHTML prop in React is named that way for a reason - it bypasses React's built-in XSS protections by directly inserting HTML into the DOM[9]. This creates potential security vulnerabilities, particularly cross-site scripting (XSS) attacks, if you're rendering untrusted content[7].

Best Practices for Safe Implementation

1. Sanitize Input Content

Always sanitize any HTML content before passing it to dangerouslySetInnerHTML. This is especially important when working with user-generated content or content from external sources[9][10].

Use established sanitization libraries:

  • DOMPurify: A robust library for sanitizing HTML
  • sanitize-html: Provides configurable HTML sanitization
  • js-xss: Specifically designed to prevent XSS attacks[10]

Example implementation with DOMPurify:

import DOMPurify from 'dompurify';
import Prism from 'prismjs';
import 'prismjs/themes/prism.css';

function CodeBlock({ code, language }) {
  const highlightedCode = Prism.highlight(
    code,
    Prism.languages[language] || Prism.languages.javascript,
    language
  );
  
  const sanitizedCode = DOMPurify.sanitize(highlightedCode);
  
  return (
    <pre>
      <code 
        className={`language-${language}`}
        dangerouslySetInnerHTML={{ __html: sanitizedCode }}
      />
    </pre>
  );
}

2. Proper PrismJS Integration

There are two main approaches to integrating PrismJS with React:

Approach 1: Using marked with PrismJS for Markdown

import React, { useEffect } from 'react';
import Prism from 'prismjs';
import 'prismjs/themes/prism.css';
import { marked } from 'marked';

function MarkdownRenderer({ markdownText }) {
  // Configure marked to use Prism for highlighting
  marked.setOptions({
    breaks: true,
    highlight: function (code, language) {
      if (Prism.languages[language]) {
        return Prism.highlight(code, Prism.languages[language], language);
      }
      return code;
    }
  });
  
  // Create renderer for additional customization if needed
  const renderer = new marked.Renderer();
  
  return (
    <div
      className="markdown-content"
      dangerouslySetInnerHTML={{
        __html: DOMPurify.sanitize(marked(markdownText, { renderer }))
      }}
    />
  );
}

This approach is particularly useful when you need to render markdown content with code blocks that require syntax highlighting[5].

Approach 2: Direct PrismJS Highlighting

For cases where you need to highlight specific code snippets:

import React, { useEffect, useRef } from 'react';
import Prism from 'prismjs';
import 'prismjs/themes/prism.css';

function CodeHighlighter({ code, language }) {
  const codeRef = useRef(null);
  
  useEffect(() => {
    if (codeRef.current) {
      Prism.highlightElement(codeRef.current);
    }
  }, [code, language]);
  
  return (
    <pre>
      <code ref={codeRef} className={`language-${language}`}>
        {code}
      </code>
    </pre>
  );
}

This approach avoids dangerouslySetInnerHTML altogether by using PrismJS's DOM manipulation directly through the highlightElement method.

3. Timing Considerations

One common issue with PrismJS in React is timing problems, where highlighting occurs before React finishes rendering. To solve this:

function CodeBlock({ code, language }) {
  const codeRef = useRef(null);
  
  useEffect(() => {
    // Ensure highlighting happens after React rendering is complete
    if (codeRef.current) {
      Prism.highlightElement(codeRef.current);
    }
  }, [code, language]);
  
  return (
    <pre>
      <code 
        ref={codeRef} 
        className={`language-${language}`}
      >
        {code}
      </code>
    </pre>
  );
}

This approach addresses the potential mismatch between React's rendering cycle and when Prism tries to highlight the code[1].

Alternative Approaches

Using Unified/Remark/Rehype

For more complex markdown processing with syntax highlighting, consider using the unified ecosystem:

import React from 'react';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypePrism from 'rehype-prism';
import rehypeReact from 'rehype-react';

function MarkdownWithSyntaxHighlighting({ markdown }) {
  const content = unified()
    .use(remarkParse)
    .use(remarkRehype)
    .use(rehypePrism)
    .use(rehypeReact, { createElement: React.createElement })
    .processSync(markdown).result;
    
  return <div className="markdown-content">{content}</div>;
}

This approach avoids dangerouslySetInnerHTML entirely by transforming markdown directly into React elements, which is more secure for user-generated content[8].

When to Avoid dangerouslySetInnerHTML

Consider alternatives to dangerouslySetInnerHTML when:

  1. Working with user-generated content that could contain malicious code
  2. You can achieve the same result using React's component composition
  3. You're building a high-security application where XSS protection is critical

In these cases, approaches like the unified/remark/rehype pipeline or direct DOM manipulation through refs might be safer options.

Conclusion

While dangerouslySetInnerHTML is sometimes necessary when working with PrismJS in React, you can mitigate risks by:

  1. Always sanitizing input content with libraries like DOMPurify
  2. Considering alternative approaches that avoid direct HTML insertion
  3. Being especially careful with user-generated content
  4. Properly timing your PrismJS highlight calls in relation to React's rendering lifecycle

By following these best practices, you can safely implement code syntax highlighting in your React applications while maintaining robust security protections.

Citations:


Sanitize highlighted code before using dangerouslySetInnerHTML

Using dangerouslySetInnerHTML without sanitization can expose users to XSS vulnerabilities. Please update the rendering in docs-v2/components/CodeBlock.js (lines 97–99):

• Import and apply DOMPurify to your highlighted HTML:

 import React from "react";
+import DOMPurify from "dompurify";
 // …
 <code
   className={`language-${language}`}
-  dangerouslySetInnerHTML={{ __html: highlightedCode }}
+  dangerouslySetInnerHTML={{
+    __html: DOMPurify.sanitize(highlightedCode),
+  }}
 />

• Alternatively, avoid dangerouslySetInnerHTML entirely by using Prism’s highlightElement with a ref:

import React, { useEffect, useRef } from "react";
import Prism from "prismjs";

function CodeBlock({ code, language }) {
  const codeRef = useRef(null);
  useEffect(() => {
    if (codeRef.current) Prism.highlightElement(codeRef.current);
  }, [code, language]);

  return (
    <pre>
      <code ref={codeRef} className={`language-${language}`}>
        {code}
      </code>
    </pre>
  );
}

Select one of these approaches to ensure your code snippets remain XSS-safe.

🧰 Tools
🪛 Biome (1.9.4)

[error] 97-97: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

/>
)
: (
<code className={`language-${language} text-gray-800 dark:text-gray-200 [text-shadow:none]`}>{code}</code>
)}
</pre>
</div>
);
}
65 changes: 65 additions & 0 deletions docs-v2/components/ConnectCodeSnippets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* This file contains all the code snippets used in the Connect SDK demo.
* Centralizing them here helps to maintain consistency and makes updates easier.
*/

/**
* Server-side code for generating a Connect token
* @param {string} externalUserId - The user's external ID
* @returns {string} The server code snippet
*/
export function getServerCodeSnippet(externalUserId) {
return `import { createBackendClient } from "@pipedream/sdk/server";

// This code runs on your server
const pd = createBackendClient({
environment: "development",
credentials: {
clientId: process.env.PIPEDREAM_CLIENT_ID,
clientSecret: process.env.PIPEDREAM_CLIENT_SECRET,
},
projectId: process.env.PIPEDREAM_PROJECT_ID
});

// Create a token for a specific user
const { token, expires_at, connect_link_url } = await pd.createConnectToken({
external_user_id: "${externalUserId || "YOUR_USER_ID"}",
});`;
}

/**
* Client-side code for connecting an account
* @param {string} appSlug - The app to connect to (slack, github, etc)
* @param {object} tokenData - The token data from the server
* @returns {string} The client code snippet
*/
export function getClientCodeSnippet(appSlug, tokenData) {
return `import { createFrontendClient } from "@pipedream/sdk/browser"

// This code runs in the frontend using the token from your server
export default function Home() {
function connectAccount() {
const pd = createFrontendClient()
pd.connectAccount({
app: "${appSlug}",
token: "${tokenData?.token
? tokenData.token
: "{connect_token}"}",
onSuccess: (account) => {
// Handle successful connection
console.log(\`Account successfully connected: \${account.id}\`)
},
onError: (err) => {
// Handle connection error
console.error(\`Connection error: \${err.message}\`)
}
})
}

return (
<main>
<button onClick={connectAccount}>Connect Account</button>
</main>
)
}`;
}
Loading
Loading