Skip to content

Commit 8b08d2d

Browse files
connect link handling, more ui polish
1 parent 0b31b6f commit 8b08d2d

File tree

4 files changed

+176
-35
lines changed

4 files changed

+176
-35
lines changed

docs-v2/components/AccountConnectionDemo.jsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export default function AccountConnectionDemo() {
2727
value={appSlug}
2828
onChange={(e) => setAppSlug(e.target.value)}
2929
className="ml-2 p-1 border rounded text-sm"
30-
disabled={!tokenData}
3130
>
3231
<option value="slack">Slack</option>
3332
<option value="github">GitHub</option>
@@ -64,12 +63,7 @@ export default function AccountConnectionDemo() {
6463
<div className="mt-4 p-3 bg-green-50 border border-green-200 text-green-800 rounded-md">
6564
<div className="font-medium text-sm">Account successfully connected!</div>
6665
<div className="mt-1 text-sm">
67-
{connectedAccount.name && (
68-
<div>Account name: <span className="font-medium">{connectedAccount.name}</span></div>
69-
)}
70-
{connectedAccount.id && (
71-
<div>Account ID: <span className="font-mono text-xs">{connectedAccount.id}</span></div>
72-
)}
66+
<div>Account ID: <span className="font-mono text-xs">{connectedAccount.id}</span></div>
7367
</div>
7468
</div>
7569
)}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"use client";
2+
3+
import {
4+
useState, useEffect,
5+
} from "react";
6+
import { useGlobalConnect } from "./GlobalConnectProvider";
7+
import CodeBlock from "./CodeBlock";
8+
9+
export default function ConnectLinkDemo() {
10+
const {
11+
tokenData, appSlug, setAppSlug,
12+
} = useGlobalConnect();
13+
const [
14+
connectLinkUrl,
15+
setConnectLinkUrl,
16+
] = useState("");
17+
18+
useEffect(() => {
19+
if (tokenData?.connect_link_url) {
20+
// Add app parameter to the URL if it doesn't already exist
21+
const baseUrl = tokenData.connect_link_url;
22+
const url = new URL(baseUrl);
23+
24+
// Update or add the app parameter
25+
url.searchParams.set("app", appSlug);
26+
27+
setConnectLinkUrl(url.toString());
28+
} else {
29+
setConnectLinkUrl("");
30+
}
31+
}, [
32+
tokenData,
33+
appSlug,
34+
]);
35+
36+
// No token data or connect_link_url - need to generate a token
37+
if (!tokenData?.connect_link_url) {
38+
return (
39+
<div className="border border-gray-200 rounded-md p-4 mt-4">
40+
<p className="text-sm text-gray-500">
41+
<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>
42+
{" "} to see a Connect Link URL here
43+
</p>
44+
</div>
45+
);
46+
}
47+
48+
return (
49+
<div className="border rounded-md overflow-hidden mt-4">
50+
<div className="bg-gray-100 border-b px-4 py-2 font-medium text-sm">
51+
Connect Link URL
52+
</div>
53+
<div className="p-4">
54+
<div className="mb-4">
55+
<label className="flex items-center mb-4">
56+
<span className="font-medium text-sm">App to connect:</span>
57+
<select
58+
value={appSlug}
59+
onChange={(e) => setAppSlug(e.target.value)}
60+
className="ml-2 p-1 border rounded text-sm"
61+
>
62+
<option value="slack">Slack</option>
63+
<option value="github">GitHub</option>
64+
<option value="google_sheets">Google Sheets</option>
65+
</select>
66+
</label>
67+
68+
<div className="mb-4">
69+
<div className="p-3 bg-gray-50 border border-gray-200 rounded-md">
70+
<code className="text-sm break-all">{connectLinkUrl}</code>
71+
</div>
72+
</div>
73+
</div>
74+
75+
<div className="flex space-x-3">
76+
<a
77+
href={connectLinkUrl}
78+
target="_blank"
79+
rel="noopener noreferrer"
80+
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"
81+
>
82+
Open Connect Link
83+
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
84+
<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" />
85+
</svg>
86+
</a>
87+
88+
<button
89+
onClick={() => {
90+
navigator.clipboard.writeText(connectLinkUrl);
91+
}}
92+
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"
93+
>
94+
Copy URL
95+
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
96+
<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" />
97+
</svg>
98+
</button>
99+
</div>
100+
101+
<div className="mt-4 text-sm text-gray-600">
102+
<p>
103+
This URL contains a Connect Token that expires in 4 hours <strong>or after it's used once</strong>.
104+
You can send this link to your users via email, SMS, or chat.
105+
</p>
106+
<p className="mt-2 text-xs text-gray-500">
107+
<strong>Note:</strong> Connect tokens are single-use. After a successful connection, you'll need to generate a new token.
108+
</p>
109+
</div>
110+
</div>
111+
</div>
112+
);
113+
}

docs-v2/components/GlobalConnectProvider.jsx

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
"use client";
22

3-
import { createContext, useContext, useState, useEffect } from "react";
3+
import {
4+
createContext, useContext, useState, useEffect,
5+
} from "react";
46
import { createFrontendClient } from "@pipedream/sdk/browser";
5-
import { getServerCodeSnippet, getClientCodeSnippet } from "./ConnectCodeSnippets";
7+
import {
8+
getServerCodeSnippet, getClientCodeSnippet,
9+
} from "./ConnectCodeSnippets";
610

711
// Generate a UUID v4 for use as external_user_id
812
function generateUUID() {
913
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
1014
const r = Math.random() * 16 | 0;
11-
const v = c === "x" ? r : (r & 0x3 | 0x8);
15+
const v = c === "x"
16+
? r
17+
: (r & 0x3 | 0x8);
1218
return v.toString(16);
1319
});
1420
}
@@ -19,16 +25,34 @@ const GlobalConnectContext = createContext(null);
1925
// Provider component
2026
export function GlobalConnectProvider({ children }) {
2127
// User and account state
22-
const [appSlug, setAppSlug] = useState("slack");
23-
const [externalUserId, setExternalUserId] = useState("");
24-
const [connectedAccount, setConnectedAccount] = useState(null);
25-
28+
const [
29+
appSlug,
30+
setAppSlug,
31+
] = useState("slack");
32+
const [
33+
externalUserId,
34+
setExternalUserId,
35+
] = useState("");
36+
const [
37+
connectedAccount,
38+
setConnectedAccount,
39+
] = useState(null);
40+
2641
// Token state
27-
const [tokenData, setTokenData] = useState(null);
28-
42+
const [
43+
tokenData,
44+
setTokenData,
45+
] = useState(null);
46+
2947
// UI state
30-
const [tokenLoading, setTokenLoading] = useState(false);
31-
const [error, setError] = useState(null);
48+
const [
49+
tokenLoading,
50+
setTokenLoading,
51+
] = useState(false);
52+
const [
53+
error,
54+
setError,
55+
] = useState(null);
3256

3357
// Generate a new UUID when the component mounts
3458
useEffect(() => {
@@ -38,14 +62,16 @@ export function GlobalConnectProvider({ children }) {
3862
// Get server code snippet wrapper function
3963
const getServerSnippet = () => getServerCodeSnippet(externalUserId);
4064

41-
// Get client code snippet wrapper function
65+
// Get client code snippet wrapper function
4266
const getClientSnippet = () => getClientCodeSnippet(appSlug, tokenData);
4367

4468
// Generate token async function
4569
async function generateToken() {
4670
setTokenLoading(true);
4771
setError(null);
48-
72+
// Clear any previously connected account when generating a new token
73+
setConnectedAccount(null);
74+
4975
try {
5076
const response = await fetch("/docs/api-demo-connect/token", {
5177
method: "POST",
@@ -56,12 +82,12 @@ export function GlobalConnectProvider({ children }) {
5682
external_user_id: externalUserId,
5783
}),
5884
});
59-
85+
6086
if (!response.ok) {
6187
const errorData = await response.json();
6288
throw new Error(errorData.error || "Failed to get token");
6389
}
64-
90+
6591
const data = await response.json();
6692
setTokenData(data);
6793
} catch (err) {
@@ -86,10 +112,14 @@ export function GlobalConnectProvider({ children }) {
86112
app: appSlug,
87113
token: tokenData.token,
88114
onSuccess: (account) => {
89-
setConnectedAccount(account);
115+
setConnectedAccount({
116+
id: account.id,
117+
});
118+
// Token is single-use, so clear it after successful connection
119+
setTokenData(null);
90120
},
91121
onError: (err) => {
92-
setError(err.message || "Failed to connect account");
122+
setError(err.message || "Failed to connect account, please refresh the page and try again.");
93123
},
94124
onClose: () => {
95125
// Dialog closed by user - no action needed
@@ -109,12 +139,12 @@ export function GlobalConnectProvider({ children }) {
109139
connectedAccount,
110140
error,
111141
tokenLoading,
112-
142+
113143
// Actions
114144
setAppSlug,
115145
generateToken,
116146
connectAccount,
117-
147+
118148
// Code snippets
119149
getServerCodeSnippet: getServerSnippet,
120150
getClientCodeSnippet: getClientSnippet,
@@ -131,7 +161,7 @@ export function GlobalConnectProvider({ children }) {
131161
export function useGlobalConnect() {
132162
const context = useContext(GlobalConnectContext);
133163
if (!context) {
134-
throw new Error('useGlobalConnect must be used within a GlobalConnectProvider');
164+
throw new Error("useGlobalConnect must be used within a GlobalConnectProvider");
135165
}
136166
return context;
137-
}
167+
}

docs-v2/pages/connect/managed-auth/quickstart.mdx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import VideoPlayer from "@/components/VideoPlayer"
55
import { GlobalConnectProvider } from '@/components/GlobalConnectProvider'
66
import TokenGenerationDemo from '@/components/TokenGenerationDemo'
77
import AccountConnectionDemo from '@/components/AccountConnectionDemo'
8+
import ConnectLinkDemo from '@/components/ConnectLinkDemo'
89

910
# Managed Auth Quickstart
1011

@@ -123,14 +124,17 @@ Use this option when you can't execute JavaScript or open an iFrame in your envi
123124

124125
The Connect Link URL opens a Pipedream-hosted page, guiding users through the account connection process. The URL is specific to the user and expires after 4 hours.
125126

126-
1. First, [generate a token](#generate-a-short-lived-token) for your users
127-
2. Check out the [optional arguments](/connect/api/#create-a-new-token) to define redirect URLs on success or error
128-
3. Extract the `connect_link_url` from the token response
129-
4. Before returning the URL to your user, add an `app` parameter to the end of the query string:
127+
After generating a token in the [step above](#generate-a-short-lived-token), you can use the resulting Connect Link URL. Try it below:
130128

131-
```
132-
https://pipedream.com/_static/connect.html?token={token}&connectLink=true&app={appSlug}&oauthAppId={oauthAppId}
133-
```
129+
<div className="not-prose">
130+
<ConnectLinkDemo />
131+
</div>
132+
133+
<Callout type="info">
134+
Make sure to add the `app` parameter to the end of the URL to specify the app.
135+
136+
Check out the [full API docs](/connect/api/#create-a-new-token) for all parameters you can pass when creating tokens, including setting redirect URLs for success or error cases.
137+
</Callout>
134138

135139
### Make authenticated requests
136140

0 commit comments

Comments
 (0)