- 
                Notifications
    
You must be signed in to change notification settings  - Fork 5.5k
 
Adding interactive Connect functionality to Connect docs #16414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 26 commits
fe072c6
              6dc6146
              0355dbd
              bdbd4a6
              c763043
              7629ea6
              7494f47
              11ea367
              d7c9c58
              44efcc1
              0b31b6f
              8b08d2d
              a5f79f3
              b156115
              67db40f
              6186932
              fd8b9ef
              9d365c8
              df2a700
              e80ca0b
              50356d8
              27ef904
              21afe03
              acaac74
              064a246
              8be5e39
              9434f9a
              8ca823a
              fc87685
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | 
| 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> | ||
| 
     | 
||
| <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> | ||
| ); | ||
| } | ||
| 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
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
 
        Suggested change
       
    
  | 
||||||||||||||||||||||||||||||||||||||||
| 
     | 
||||||||||||||||||||||||||||||||||||||||
| 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
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConsider 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  🌐 Web query: 💡 Result: Using dangerouslySetInnerHTML with PrismJS in React SafelyWhen combining PrismJS for syntax highlighting with React components, you'll often need to use  Understanding the RisksThe  Best Practices for Safe Implementation1. Sanitize Input ContentAlways sanitize any HTML content before passing it to  Use established sanitization libraries: 
 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 IntegrationThere are two main approaches to integrating PrismJS with React: Approach 1: Using marked with PrismJS for Markdownimport 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 HighlightingFor 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  3. Timing ConsiderationsOne 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 ApproachesUsing Unified/Remark/RehypeFor 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  When to Avoid dangerouslySetInnerHTMLConsider alternatives to  
 In these cases, approaches like the unified/remark/rehype pipeline or direct DOM manipulation through refs might be safer options. ConclusionWhile  
 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  • 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  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> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| 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> | ||
| ) | ||
| }`; | ||
| } | 
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||||||
| "use client"; | ||||||||||
| 
     | 
||||||||||
| import { | ||||||||||
| useState, | ||||||||||
| useEffect, | ||||||||||
| } from "react"; | ||||||||||
| import { useGlobalConnect } from "./GlobalConnectProvider"; | ||||||||||
| import { styles } from "../utils/componentStyles"; | ||||||||||
| 
     | 
||||||||||
| export default function ConnectLinkDemo() { | ||||||||||
| const { | ||||||||||
| tokenData, | ||||||||||
| appSlug, | ||||||||||
| setAppSlug, | ||||||||||
| } = useGlobalConnect(); | ||||||||||
| const [ | ||||||||||
| connectLinkUrl, | ||||||||||
| setConnectLinkUrl, | ||||||||||
| ] = useState(""); | ||||||||||
| 
     | 
||||||||||
| useEffect(() => { | ||||||||||
| if (tokenData?.connect_link_url) { | ||||||||||
| // Add app parameter to the URL if it doesn't already exist | ||||||||||
| const baseUrl = tokenData.connect_link_url; | ||||||||||
| const url = new URL(baseUrl); | ||||||||||
| 
     | 
||||||||||
| // Update or add the app parameter | ||||||||||
| url.searchParams.set("app", appSlug); | ||||||||||
| 
     | 
||||||||||
| setConnectLinkUrl(url.toString()); | ||||||||||
| } else { | ||||||||||
| setConnectLinkUrl(""); | ||||||||||
| } | ||||||||||
| }, [ | ||||||||||
| tokenData, | ||||||||||
| appSlug, | ||||||||||
| ]); | ||||||||||
| 
     | 
||||||||||
| // No token data or connect_link_url - need to generate a token | ||||||||||
| if (!tokenData?.connect_link_url) { | ||||||||||
| return ( | ||||||||||
| <div className={`${styles.container} p-4`}> | ||||||||||
| <p className={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> | ||||||||||
| {" "} to see a Connect Link URL here | ||||||||||
| </p> | ||||||||||
| </div> | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| return ( | ||||||||||
| <div className={styles.container}> | ||||||||||
| <div className={styles.header}> | ||||||||||
| Connect Link URL | ||||||||||
| </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="slack">Slack</option> | ||||||||||
| <option value="github">GitHub</option> | ||||||||||
| <option value="google_sheets">Google Sheets</option> | ||||||||||
| </select> | ||||||||||
| </label> | ||||||||||
| 
     | 
||||||||||
| <div className="mb-4"> | ||||||||||
| <div className={styles.codeDisplay}> | ||||||||||
| <code className={styles.codeText}>{connectLinkUrl}</code> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| 
     | 
||||||||||
| <div className="flex space-x-3"> | ||||||||||
| <a | ||||||||||
| href={connectLinkUrl} | ||||||||||
| target="_blank" | ||||||||||
| rel="noopener noreferrer" | ||||||||||
| className={`${styles.primaryButton} inline-flex items-center`} | ||||||||||
| > | ||||||||||
| Open Connect Link | ||||||||||
| <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||||||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> | ||||||||||
| </svg> | ||||||||||
| </a> | ||||||||||
| 
     | 
||||||||||
| <button | ||||||||||
| onClick={() => { | ||||||||||
| navigator.clipboard.writeText(connectLinkUrl); | ||||||||||
| }} | ||||||||||
| className={styles.secondaryButton} | ||||||||||
| > | ||||||||||
| Copy URL | ||||||||||
| <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||||||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /> | ||||||||||
| </svg> | ||||||||||
| </button> | ||||||||||
| </div> | ||||||||||
| 
     | 
||||||||||
| <div className={`mt-4 ${styles.text.normal}`}> | ||||||||||
| <p> | ||||||||||
| This URL contains a Connect Token that expires in 4 hours | ||||||||||
| <strong className={styles.text.strong}> or after it's used once</strong>. | ||||||||||
| You can send this link to your users via email, SMS, or chat. | ||||||||||
| </p> | ||||||||||
| <p className={`mt-2 ${styles.text.small}`}> | ||||||||||
| <strong className={styles.text.strongMuted}>Note:</strong> Connect tokens are single-use. After a successful connection, | ||||||||||
| you'll need to generate a new token. | ||||||||||
                
       | 
||||||||||
| <strong className={styles.text.strongMuted}>Note:</strong> Connect tokens are single-use. After a successful connection, | |
| you'll need to generate a new token. | |
| <strong className={styles.text.strongMuted}>Note:</strong> Connect tokens are single-use. | |
| After a successful connection, you'll need to generate a new token. | 
🧰 Tools
🪛 GitHub Check: Lint Code Base
[failure] 111-111:
This line has a length of 132. Maximum allowed is 100
🪛 GitHub Actions: Pull Request Checks
[error] 111-111: ESLint: This line has a length of 132. Maximum allowed is 100. (max-len)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default appSlug (
"slack") is missing from the<select>listGlobalConnectProviderinitialisesappSlugwith"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