├── public └── favicon.ico ├── .eslintrc.json ├── linkedin_sample_app_banner.png ├── linkedin_sample_app_provider_setup.png ├── .env.local.example ├── renovate.json ├── next.config.js ├── cypress ├── fixtures │ └── example.json ├── e2e │ └── programmatic_login_spec.cy.ts └── support │ ├── e2e.ts │ └── commands.ts ├── middleware.ts ├── styles ├── Login.module.css ├── globals.css └── Home.module.css ├── .gitignore ├── cypress.config.ts ├── layout.tsx ├── app ├── layout.tsx ├── api │ └── getPost │ │ └── route.ts └── page.tsx ├── package.json ├── tsconfig.json ├── LICENSE ├── README.md └── yarn.lock /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/linkedin-sample-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/display-name": "off" 5 | } 6 | } -------------------------------------------------------------------------------- /linkedin_sample_app_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/linkedin-sample-app/HEAD/linkedin_sample_app_banner.png -------------------------------------------------------------------------------- /linkedin_sample_app_provider_setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/linkedin-sample-app/HEAD/linkedin_sample_app_provider_setup.png -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_DESCOPE_PROJECT_ID="" 2 | SIGN_IN_ROUTE="" 3 | NEXT_PUBLIC_DESCOPE_FLOW_ID="" 4 | NEXT_PUBLIC_DESCOPE_BASE_URL="" 5 | DESCOPE_MANAGEMENT_KEY="" -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>descope-sample-apps/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | //swcMinify: true, 5 | }; 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@descope/nextjs-sdk/server"; 2 | 3 | export default authMiddleware({ 4 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, 5 | redirectUrl: "/", 6 | publicRoutes: ["/"], 7 | }); 8 | 9 | export const config = { 10 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/(api|trpc)(.*)"], 11 | }; 12 | -------------------------------------------------------------------------------- /styles/Login.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | min-height: 100vh; 3 | padding: 4rem 0; 4 | flex: 1; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .title { 12 | margin: 0; 13 | line-height: 1.15; 14 | font-size: 4rem; 15 | } 16 | 17 | .login { 18 | flex: 1; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | flex-wrap: wrap; 23 | width: 500px; 24 | } 25 | -------------------------------------------------------------------------------- /cypress/e2e/programmatic_login_spec.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Descope', function () { 2 | beforeEach(function () { 3 | cy.deleteAllTestUsers() 4 | cy.loginViaDescopeAPI() 5 | }) 6 | 7 | it('shows test user welcome message', function () { 8 | // cy.contains('Hello Test User').should('be.visible') 9 | }) 10 | 11 | it('validates api request', function () { 12 | // cy.get('[data-cy=api-form-button]').click() 13 | // cy.contains('Result: Request Validated').should('be.visible'); 14 | }) 15 | }) -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | import * as dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export default defineConfig({ 7 | 8 | e2e: { 9 | includeShadowDom: true, // Important for interacting with Descope components 10 | baseUrl: 'http://localhost:3000', 11 | setupNodeEvents(on, config) { 12 | // implement node event listeners here 13 | }, 14 | }, 15 | env: { 16 | descope_project_id: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, 17 | descope_management_key: process.env.DESCOPE_MANAGEMENT_KEY 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { AuthProvider } from "@descope/nextjs-sdk"; 5 | import { Inter as FontSans } from "next/font/google"; 6 | 7 | const fontSans = FontSans({ 8 | subsets: ["latin"], 9 | variable: "--font-sans", 10 | }); 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | return ( 18 | 19 | 23 | {children} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { AuthProvider } from "@descope/nextjs-sdk"; 5 | import { Inter as FontSans } from "next/font/google"; 6 | 7 | const fontSans = FontSans({ 8 | subsets: ["latin"], 9 | variable: "--font-sans", 10 | }); 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | return ( 18 | 19 | 23 | {children} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkedin-sample-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@descope/nextjs-sdk": "^0.3.20", 13 | "@types/node": "18.17.11", 14 | "@types/react": "18.2.21", 15 | "@types/react-dom": "18.2.7", 16 | "axios": "^1.7.9", 17 | "dotenv": "^16.3.1", 18 | "eslint": "8.48.0", 19 | "eslint-config-next": "13.4.19", 20 | "next": "15.5.9", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "typescript": "5.1.6" 24 | }, 25 | "devDependencies": { 26 | "cypress": "^15.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Descope Sample App 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. 22 | -------------------------------------------------------------------------------- /app/api/getPost/route.ts: -------------------------------------------------------------------------------- 1 | import { createSdk, session } from "@descope/nextjs-sdk/server"; 2 | import axios from "axios"; 3 | 4 | const descopeClient = createSdk({ 5 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, 6 | managementKey: process.env.DESCOPE_MANAGEMENT_KEY, 7 | }); 8 | 9 | export async function POST(request: Request) { 10 | const requestBody = await request.json(); 11 | const user = requestBody.user; 12 | const postText = requestBody.postText; 13 | 14 | // loginId (str): The login_id of the user to be loaded. 15 | const loginId = user.id; 16 | // provider (str): The provider name (google, facebook, etc') 17 | const provider = "linkedin"; 18 | const resp = await descopeClient.management.user.getProviderToken( 19 | loginId, 20 | provider 21 | ); 22 | if (!resp.ok) { 23 | return new Response("not able to get provider token", { status: 401 }); 24 | } 25 | const providerToken = resp.data; 26 | 27 | // LinkedIn API endpoint for creating posts 28 | const linkedInPostUrl = "https://api.linkedin.com/v2/ugcPosts"; 29 | 30 | // Define the content of the post 31 | const postData = { 32 | author: `urn:li:person:${providerToken?.providerUserId}`, 33 | lifecycleState: "PUBLISHED", 34 | specificContent: { 35 | "com.linkedin.ugc.ShareContent": { 36 | shareCommentary: { 37 | text: postText, 38 | }, 39 | shareMediaCategory: "NONE", 40 | }, 41 | }, 42 | visibility: { 43 | "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC", 44 | }, 45 | }; 46 | 47 | try { 48 | // Make a POST request to the LinkedIn API 49 | const response = await axios.post(linkedInPostUrl, postData, { 50 | headers: { 51 | Authorization: `Bearer ${providerToken?.accessToken}`, 52 | "Content-Type": "application/json", 53 | "X-Restli-Protocol-Version": "2.0.0", 54 | }, 55 | }); 56 | 57 | return new Response(JSON.stringify({ data: "Posting Succeeded" }), { 58 | status: 200, 59 | }); 60 | } catch (error) { 61 | return new Response(JSON.stringify({ data: "Posting Failed" }), { 62 | status: 401, 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .title a { 16 | color: #0070f3; 17 | text-decoration: none; 18 | } 19 | 20 | .title a:hover, 21 | .title a:focus, 22 | .title a:active { 23 | text-decoration: underline; 24 | } 25 | 26 | .title { 27 | line-height: 1.15; 28 | font-size: 4rem; 29 | } 30 | 31 | .title, 32 | .description { 33 | text-align: center; 34 | } 35 | 36 | .description { 37 | line-height: 1.5; 38 | font-size: 1.5rem; 39 | } 40 | 41 | .code { 42 | background: #fafafa; 43 | border-radius: 5px; 44 | padding: 0.75rem; 45 | font-size: 1.1rem; 46 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 47 | Bitstream Vera Sans Mono, Courier New, monospace; 48 | } 49 | 50 | .grid { 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | flex-wrap: wrap; 55 | flex-direction: column; 56 | max-width: 800px; 57 | } 58 | 59 | .centeredButton { 60 | display: block; 61 | margin: 0 auto; 62 | } 63 | 64 | .card { 65 | margin: 1rem; 66 | padding: 1.5rem; 67 | text-align: left; 68 | color: inherit; 69 | text-decoration: none; 70 | border: 1px solid #eaeaea; 71 | border-radius: 10px; 72 | transition: color 0.15s ease, border-color 0.15s ease; 73 | max-width: 300px; 74 | } 75 | 76 | 77 | 78 | @media (prefers-color-scheme: dark) { 79 | .card, 80 | .code { 81 | background: #111; 82 | } 83 | } 84 | 85 | .main { 86 | min-height: 100vh; 87 | padding: 4rem 0; 88 | flex: 1; 89 | display: flex; 90 | flex-direction: column; 91 | justify-content: center; 92 | align-items: center; 93 | } 94 | 95 | .title { 96 | margin: 0; 97 | line-height: 1.15; 98 | font-size: 4rem; 99 | } 100 | 101 | .login { 102 | flex: 1; 103 | display: flex; 104 | align-items: center; 105 | justify-content: center; 106 | flex-wrap: wrap; 107 | width: 500px; 108 | } 109 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useDescope, useSession, useUser } from "@descope/nextjs-sdk/client"; 4 | import Head from "next/head"; 5 | import Link from "next/link"; 6 | import { SyntheticEvent, useCallback, useState } from "react"; 7 | import styles from "../styles/Home.module.css"; 8 | import { Descope } from "@descope/nextjs-sdk"; 9 | 10 | 11 | const getUserDisplayName = (user: any) => 12 | user?.name || user?.externalIds?.[0] || ""; 13 | 14 | export default function Home() { 15 | const { isAuthenticated } = useSession(); 16 | const { user } = useUser(); 17 | const { logout } = useDescope(); 18 | 19 | const onLogout = useCallback(() => { 20 | logout(); 21 | }, [logout]); 22 | const [postText, setPostText] = useState(""); // State to hold the post content 23 | 24 | const handleSubmit = async (event: SyntheticEvent) => { 25 | event.preventDefault(); 26 | 27 | const url = new URL("/api/getPost", window.location.origin); 28 | const requestBody = { 29 | user: { id: user.loginIds[0]}, 30 | postText: postText, 31 | }; 32 | 33 | const response = await fetch(url, { 34 | method: "POST", 35 | headers: { 36 | "Content-Type": "application/json", 37 | }, 38 | body: JSON.stringify(requestBody), 39 | }); 40 | 41 | const result = await response.json(); 42 | console.log(result); 43 | const resultMessage = `${result.data}`; 44 | alert(resultMessage); 45 | }; 46 | 47 | return ( 48 |
49 | 50 | Create Next App 51 | 52 | 53 | 54 | 55 |
56 |

Welcome!

57 | {!isAuthenticated && ( 58 | 63 | )} 64 | {isAuthenticated && ( 65 | <> 66 |
67 | Hello {getUserDisplayName(user)} 68 |
69 | 70 |
71 |
Make A Linkedin Post
72 |
73 |