├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── callback │ ├── actions │ │ └── exchange-code-for-access-token.ts │ └── page.tsx ├── configure │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── bun.lockb ├── next.config.js ├── package.json ├── postcss.config.js ├── public └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.env.local.example: -------------------------------------------------------------------------------- 1 | CLIENT_ID= 2 | CLIENT_SECRET= 3 | HOST=http://localhost:3000 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vercel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vercel Example Integration 2 | 3 | This app is an example integration, built with Next.js. 4 | 5 | It shows: 6 | 7 | - how to exchange the `code` for an `access_token` to interact with the API 8 | - how to use the API to display all projects for the current user or team 9 | 10 | 11 | ![image](https://user-images.githubusercontent.com/7249920/110459590-7389e500-80cd-11eb-9258-6d6d229c7a50.png) 12 | 13 | 14 | ## Run this example 15 | 16 | 1. Create a new integration on the [integration console](https://vercel.com/dashboard/integrations/console) 17 | 18 | 2. Set the Redirect URL to `http://localhost:3000/callback` 19 | 20 | 3. Set the environment variables: 21 | 22 | ``` 23 | cp .env.local.example .env.local 24 | ``` 25 | 26 | Set the `CLIENT_ID` and `CLIENT_SECRET` accordingly to the values you see in the integration console if you edit your integration. 27 | 28 | 4. Install all dependencies 29 | 30 | ``` 31 | npm install 32 | ``` 33 | 34 | 5. Start the app 35 | 36 | ``` 37 | npm run dev 38 | ``` 39 | 40 | 6. Add it to a project 41 | 42 | Now your example integration is running on `http://localhost:3000`. Click on "View in Marketplace" to see your integration with all details like others will see it. You're now able to add your integration to a project. Once you click "add" you see a popup that will use the defined Redirect URL `http://localhost:3000/callback`. The integration is now installed. 43 | 44 | 45 | 46 | ## How this integration works 47 | 48 | 1. The user clicks "add" and selects the scope 49 | 2. The user sees the callback popup with your defined Redirect URL 50 | 3. The Redirect URL will be called with query parameters that we can use: 51 | - `code`: The authorization code to receive an `access_token` in order to interact with the API 52 | - `teamId`: The id of the team (only provided if the integration gets installed on a team) 53 | - `configurationId`: The id of the installation (you usually want to store this information) 54 | - `next`: The URL we're redirecting if the setup is done 55 | 4. Once the user sees the page `/setup` we exchange the provided `code` for an `access_token`. See the docs for [exchanging code for an access token](https://vercel.com/docs/rest-api/vercel-api-integrations#exchange-code-for-access-token) 56 | 5. After the `code` was exchanged, we can use the `access_token` for our calls to the Vercel API. See the docs for [available endpoints](https://vercel.com/docs/api#endpoints). In this case we're querying the [Projects endpoint](https://vercel.com/docs/api#endpoints/projects/get-projects) to get a list of all projects for the user or the team 57 | 6. The user sees a list of projects. This would be the step to provide additional information and allow the user to link projects to your own resources. 58 | 7. The user clicks on "Redirect me back to Vercel" to close the popup and complete the installation on Vercel. In your real integration, this should be done automatically after you collected all information you need, to save the user some clicks. 59 | 60 | 61 | 62 | **Important note:** 63 | 64 | Please make sure, that you provide the `teamId` as a query parameter while interacting with the API. To determine if you have to add a `teamId` to API calls, see the response after exchanging the `code ` for an `access_token`. See the docs for [accessing resources owned by a team](https://vercel.com/docs/api#api-basics/authentication/accessing-resources-owned-by-a-team). 65 | -------------------------------------------------------------------------------- /app/callback/actions/exchange-code-for-access-token.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import qs from "querystring"; 4 | 5 | export async function exchangeCodeForAccessToken(code: string) { 6 | /* 7 | Exchange the code for a long-lived access token. 8 | Note: This call can only be made once per code. 9 | */ 10 | const result = await fetch("https://api.vercel.com/v2/oauth/access_token", { 11 | headers: { 12 | "Content-Type": "application/x-www-form-urlencoded", 13 | }, 14 | method: "POST", 15 | body: qs.stringify({ 16 | client_id: process.env.CLIENT_ID, 17 | client_secret: process.env.CLIENT_SECRET, 18 | code, 19 | redirect_uri: `${process.env.HOST}/callback`, // This parameter should match the Redirect URL in your integration settings on Vercel 20 | }), 21 | }); 22 | 23 | const body = await result.json(); 24 | 25 | if (process.env.NODE_ENV !== "production") { 26 | console.log( 27 | "https://api.vercel.com/v2/oauth/access_token returned:", 28 | JSON.stringify(body, null, 2) 29 | ); 30 | } 31 | 32 | return body as { 33 | token_type: string; 34 | access_token: string; 35 | installation_id: string; 36 | user_id: string; 37 | team_id: string | null; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /app/callback/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | import { useEffect, useState, useTransition } from "react"; 5 | import { exchangeCodeForAccessToken } from "~/app/callback/actions/exchange-code-for-access-token"; 6 | 7 | // The URL of this page should be added as Redirect URL in your integration settings on Vercel 8 | export default function Page() { 9 | const [accessToken, setAccessToken] = useState(); 10 | const [scope, setScope] = useState<"team" | "personal account" | undefined>(); 11 | 12 | const searchParams = useSearchParams(); 13 | const code = searchParams.get("code"); 14 | const next = searchParams.get("next"); 15 | 16 | const [_, exchange] = useTransition(); 17 | 18 | useEffect(() => { 19 | if (!code) return; 20 | 21 | exchange(async () => { 22 | const result = await exchangeCodeForAccessToken(code); 23 | 24 | /* 25 | Important: This is only for demonstration purposes. 26 | The access token should never be sent to the client. All API calls to the Vercel API should be made from the server. 27 | Therefore, we use a server action to exchange the code for an access token. 28 | */ 29 | setAccessToken(result.access_token); 30 | setScope(result.team_id ? "team" : "personal account"); 31 | }); 32 | }, [code]); 33 | 34 | return ( 35 |
36 |
37 |

Integration is installed on a

38 | 39 | {accessToken ? ( 40 |
41 | {scope} 42 |
43 | ) : null} 44 |
45 | 46 | {/* 47 | Important: The access token is displayed for demonstration purposes only and should never be sent to the client in a production environment. 48 | Depending on the scopes assigned to your integration, you can use this access token to call the corresponding API endpoints. 49 | */} 50 |
51 |

Vercel Access Token:

52 |
53 | 54 | {accessToken ? accessToken : "Loading..."} 55 | 56 |
57 |
58 | 59 |
60 | 64 | {/* 65 | If you have finished handling the installation, the redirect should happen programmatically 66 | */} 67 | Redirect me back to Vercel 68 | 69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/configure/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | 5 | // The URL of this page should be added as Configuration URL in your integration settings on Vercel 6 | export default function Page() { 7 | const searchParams = useSearchParams(); 8 | 9 | /* 10 | The configurationId is passed as a query parameter, use it get additional information about the installation. 11 | The configurationId should always be verified, for example, by checking it against the currently logged-in user, to not leak any information. 12 | */ 13 | const configurationId = searchParams.get("configurationId"); 14 | 15 | return ( 16 |
17 |

Example Integration

18 |

19 | This page is used to show some configuration options for the 20 | configuration with the id{" "} 21 | 22 | {configurationId} 23 | 24 |

25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/example-integration/793c257b238a23fd8649d46c97e153f7877fa184/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import Image from "next/image"; 3 | import "./globals.css"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Vercel Example Integration", 7 | }; 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | 16 | 17 |
18 | {children} 19 |
20 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return

Vercel Example Integration

; 3 | } 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/example-integration/793c257b238a23fd8649d46c97e153f7877fa184/bun.lockb -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vercel-example-integration", 3 | "version": "2.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "react": "^18", 13 | "react-dom": "^18", 14 | "next": "14.0.1" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5", 18 | "@types/node": "^20", 19 | "@types/react": "^18", 20 | "@types/react-dom": "^18", 21 | "autoprefixer": "^10.0.1", 22 | "postcss": "^8", 23 | "tailwindcss": "^3.3.0", 24 | "eslint": "^8", 25 | "eslint-config-next": "14.0.1" 26 | } 27 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: {}, 10 | plugins: [], 11 | }; 12 | export default config; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "~/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------