-
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 24 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,88 @@ | ||
| "use client"; | ||
|
|
||
| import { useGlobalConnect } from "./GlobalConnectProvider"; | ||
| import CodeBlock from "./CodeBlock"; | ||
|
|
||
| export default function AccountConnectionDemo() { | ||
| const { | ||
| appSlug, | ||
| setAppSlug, | ||
| tokenData, | ||
| getClientCodeSnippet, | ||
| connectAccount, | ||
| connectedAccount, | ||
| error, | ||
| } = useGlobalConnect(); | ||
|
|
||
| return ( | ||
| <div className="border rounded-md overflow-hidden mt-4"> | ||
| <div className="bg-gray-100 border-b px-4 py-2 font-medium text-sm"> | ||
| Connect an account from your frontend | ||
| </div> | ||
| <div className="p-4"> | ||
| <div className="mb-4"> | ||
| <label className="flex items-center mb-4"> | ||
| <span className="font-medium text-sm">App to connect:</span> | ||
| <select | ||
| value={appSlug} | ||
| onChange={(e) => setAppSlug(e.target.value)} | ||
| className="ml-2 p-1 border rounded text-sm" | ||
| > | ||
| <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="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors disabled:opacity-50 font-medium text-sm" | ||
| > | ||
| Connect Account | ||
| </button> | ||
| {!tokenData && <p className="mt-2 text-sm text-gray-500"><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="mt-4 p-3 bg-red-50 border border-red-200 text-red-800 rounded-md"> | ||
| <div className="font-medium text-sm">Error</div> | ||
| <div className="mt-1 text-sm">{error}</div> | ||
| </div> | ||
| )} | ||
|
|
||
| {connectedAccount && ( | ||
| <div className="mt-4 p-3 bg-green-50 border border-green-200 text-green-800 rounded-md"> | ||
| <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,108 @@ | ||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| useState, useEffect, | ||||||||||||||||||||||||||||||||||||||||
| } from "react"; | ||||||||||||||||||||||||||||||||||||||||
| 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 bg-gray-50 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 flex items-center justify-center text-gray-500 hover:bg-gray-200 hover:text-gray-700 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`} | ||||||||||||||||||||||||||||||||||||||||
| 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`}>{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,117 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useState, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useGlobalConnect } from "./GlobalConnectProvider"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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="border border-gray-200 rounded-md p-4 mt-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-sm text-gray-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <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="border rounded-md overflow-hidden mt-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="bg-gray-100 border-b px-4 py-2 font-medium text-sm"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Connect Link URL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mb-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label className="flex items-center mb-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="font-medium text-sm">App to connect:</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <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="github">GitHub</option> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <option value="google_sheets">Google Sheets</option> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </select> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mb-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-3 bg-gray-50 border border-gray-200 rounded-md"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <code className="text-sm break-all">{connectLinkUrl}</code> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex space-x-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| href={connectLinkUrl} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target="_blank" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rel="noopener noreferrer" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors disabled:opacity-50 font-medium text-sm 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="px-4 py-2 bg-gray-100 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-200 transition-colors font-medium text-sm inline-flex items-center" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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 user feedback for clipboard operations. The copy to clipboard button doesn't provide any feedback to the user upon successful copying. Consider adding a temporary visual indicator. + const [copyFeedback, setCopyFeedback] = useState(false);
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(connectLinkUrl)
+ .then(() => {
+ setCopyFeedback(true);
+ setTimeout(() => setCopyFeedback(false), 2000);
+ })
+ .catch(err => console.error('Failed to copy URL:', err));
+ };
<button
- onClick={() => {
- navigator.clipboard.writeText(connectLinkUrl);
- }}
+ onClick={handleCopy}
className="px-4 py-2 bg-gray-100 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-200 transition-colors font-medium text-sm inline-flex items-center"
>
- Copy URL
+ {copyFeedback ? "Copied!" : "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>📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-4 text-sm text-gray-600"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This URL contains a Connect Token that expires in 4 hours | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <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 text-xs text-gray-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <strong>Note:</strong> Connect tokens are single-use. After a successful connection, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| you'll need to generate a new token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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