├── .env.local ├── .gitignore ├── README.md ├── app.json ├── components ├── Layout.jsx ├── Layout.module.css ├── TodoApp.jsx └── TodoModel.js ├── hooks └── useParagon.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── integrations.js └── tasks.js ├── public ├── favicon.ico └── vercel.svg ├── server.js └── styles ├── Home.module.css ├── Integrations.module.css └── globals.css /.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_PARAGON_PROJECT_ID="" 2 | PARAGON_SIGNING_KEY="" 3 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paragon Sample App 2 | 3 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/useparagon/paragon-connect-nextjs-example) 4 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fuseparagon%2Fparagon-connect-nextjs-example&env=NEXT_PUBLIC_PARAGON_PROJECT_ID,PARAGON_SIGNING_KEY&envDescription=You%20can%20find%20your%20Paragon%20Project%20ID%20in%20the%20Paragon%20Dashboard%2C%20in%20any%20integration's%20Overview%20tab.%20You%20can%20create%20your%20Paragon%20Signing%20Key%20in%20the%20Settings%20tab%20of%20the%20Paragon%20dashboard.&envLink=https%3A%2F%2Fbit.ly%2F3c4MytJ&demo-title=Paragon%20-%20Sample%20App&demo-description=A%20demo%20Next.js%20app%2C%20with%20integrations%20built%20on%20Paragon.&demo-url=https%3A%2F%2Fparagon-demo-live.vercel.app%2F&demo-image=https%3A%2F%2Fuseparagon.notion.site%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252Fd28afd31-7e3e-4f05-8b2f-a59f8598baac%252FFrame_977.png%3Ftable%3Dblock%26id%3Dd11fb8bc-ad8c-48fa-b0a7-1ea1dff1f526%26spaceId%3D731f846c-b074-4391-8301-e3172493b9f1%26width%3D1870%26userId%3D%26cache%3Dv2) 5 | 6 | This is an example app based on [TodoMVC in React](https://github.com/tastejs/todomvc/tree/gh-pages/examples/react/js) and the Next.js starter project. 7 | 8 | For more info on adding Paragon to your app, see: 9 | 10 | - 🎥 **[Implementation walkthrough video](https://youtu.be/BcwOUMRXg_k?t=177)** (~5m): Watch how to add the client-side SDK, generate the Paragon User Token, and interact with the Events API. This repo is the code for TaskLab, featured in the demo! 11 | 12 | - 📄 **[Setup documentation](https://docs.useparagon.com/v/connect/getting-started/installing-the-connect-sdk)**: Get step-by-step instructions adding the SDK in many common app authentication scenarios. 13 | 14 | # Getting started 15 | 16 | ## Configuration 17 | 18 | To use all of the features of this demo, you will need a Paragon account. 19 | 20 | Paste in your Project ID and Signing Key into the values of `.env.local`, at the root of the repository. 21 | 22 | ``` 23 | NEXT_PUBLIC_PARAGON_PROJECT_ID="" 24 | PARAGON_SIGNING_KEY="" 25 | ``` 26 | 27 | ## Installation 28 | 29 | This demo requires [Node.js](https://nodejs.org) (>= 16.x) to be installed. 30 | 31 | Install dependencies: 32 | 33 | ``` 34 | npm install 35 | ``` 36 | 37 | Start the application dev server: 38 | 39 | ``` 40 | npm start 41 | ``` 42 | 43 | After the demo has started, it will print the local URL you can visit: 44 | 45 | ``` 46 | > Ready on http://localhost:3000 47 | ``` 48 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Paragon - Sample App", 3 | "description": "A demo Next.js app, with integrations built on Paragon", 4 | "repository": "https://github.com/paragon/paragon-connect-nextjs-example", 5 | "logo": "https://useparagon.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F993639c4-2223-4ae3-a2c1-b6fe38bd4cd6%2Fwebclip.png?table=block&id=f37c07ca-3b88-47ca-9dcc-2e0ecef8190c&spaceId=731f846c-b074-4391-8301-e3172493b9f1&width=1540&userId=&cache=v2", 6 | "keywords": ["integrations", "next.js"], 7 | "env": { 8 | "NEXT_PUBLIC_PARAGON_PROJECT_ID": { 9 | "description": "Your Paragon Project ID, which can be found in the Paragon dashboard in any integration.", 10 | "required": true 11 | }, 12 | "PARAGON_SIGNING_KEY": { 13 | "description": "Your Paragon Signing Key, which can be created in the Settings tab of the Paragon dashboard.", 14 | "required": true 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | import styles from "./Layout.module.css"; 5 | 6 | const Layout = ({ children, title }) => { 7 | const router = useRouter(); 8 | 9 | return ( 10 |
11 |
12 |

TaskLab

13 |

{title}

14 |
15 |
16 | 28 | {children} 29 |
30 | ); 31 | }; 32 | 33 | export default Layout; 34 | -------------------------------------------------------------------------------- /components/Layout.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background-color: white; 3 | box-shadow: 0px 10px 15px rgba(226, 235, 248, 0.1); 4 | border-bottom: 1px solid #e2ebf8; 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | width: 100vw; 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | padding: 0 30px; 13 | box-sizing: border-box; 14 | font-weight: 700; 15 | z-index: 9999; 16 | } 17 | 18 | .header > * { 19 | flex: 1; 20 | } 21 | 22 | .header img { 23 | border-radius: 100%; 24 | height: 35px; 25 | margin-left: 30px; 26 | } 27 | 28 | .nav { 29 | position: fixed; 30 | top: 0; 31 | left: 0; 32 | width: 150px; 33 | height: 100vh; 34 | background-color: white; 35 | border-right: 1px solid #e2ebf8; 36 | padding: 80px 30px; 37 | } 38 | 39 | .nav > * { 40 | display: block; 41 | margin-bottom: 20px; 42 | text-decoration: none; 43 | font-weight: 500; 44 | color: #737c86; 45 | } 46 | 47 | .active { 48 | font-weight: 600; 49 | color: #333; 50 | } 51 | -------------------------------------------------------------------------------- /components/TodoApp.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "classnames"; 3 | import { Utils } from "./TodoModel"; 4 | 5 | var ESCAPE_KEY = 27; 6 | var ENTER_KEY = 13; 7 | 8 | const ALL_TODOS = "all"; 9 | const ACTIVE_TODOS = "active"; 10 | const COMPLETED_TODOS = "completed"; 11 | 12 | class TodoItem extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { editText: this.props.todo.title }; 16 | } 17 | 18 | handleSubmit(event) { 19 | var val = this.state.editText.trim(); 20 | if (val) { 21 | this.props.onSave(val); 22 | this.setState({ editText: val }); 23 | } else { 24 | this.props.onDestroy(); 25 | } 26 | } 27 | 28 | handleEdit() { 29 | this.props.onEdit(); 30 | this.setState({ editText: this.props.todo.title }); 31 | } 32 | 33 | handleKeyDown(event) { 34 | if (event.which === ESCAPE_KEY) { 35 | this.setState({ editText: this.props.todo.title }); 36 | this.props.onCancel(event); 37 | } else if (event.which === ENTER_KEY) { 38 | this.handleSubmit(event); 39 | } 40 | } 41 | 42 | handleChange(event) { 43 | if (this.props.editing) { 44 | this.setState({ editText: event.target.value }); 45 | } 46 | } 47 | 48 | /** 49 | * This is a completely optional performance enhancement that you can 50 | * implement on any React component. If you were to delete this method 51 | * the app would still work correctly (and still be very performant!), we 52 | * just use it as an example of how little code it takes to get an order 53 | * of magnitude performance improvement. 54 | */ 55 | shouldComponentUpdate(nextProps, nextState) { 56 | return ( 57 | nextProps.todo !== this.props.todo || 58 | nextProps.editing !== this.props.editing || 59 | nextState.editText !== this.state.editText 60 | ); 61 | } 62 | 63 | /** 64 | * Safely manipulate the DOM after updating the state when invoking 65 | * `this.props.onEdit()` in the `handleEdit` method above. 66 | * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate 67 | * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate 68 | */ 69 | componentDidUpdate(prevProps) { 70 | if (!prevProps.editing && this.props.editing) { 71 | var node = React.findDOMNode(this.refs.editField); 72 | node.focus(); 73 | node.setSelectionRange(node.value.length, node.value.length); 74 | } 75 | } 76 | 77 | render() { 78 | return ( 79 |
  • 85 |
    86 | 92 | 93 |
    95 | 103 |
  • 104 | ); 105 | } 106 | } 107 | 108 | class TodoFooter extends React.Component { 109 | render() { 110 | var activeTodoWord = Utils.pluralize(this.props.count, "item"); 111 | var clearButton = null; 112 | 113 | if (this.props.completedCount > 0) { 114 | clearButton = ( 115 | 121 | ); 122 | } 123 | 124 | var nowShowing = this.props.nowShowing; 125 | return ( 126 | 160 | ); 161 | } 162 | } 163 | 164 | class TodoApp extends React.Component { 165 | constructor(props) { 166 | super(props); 167 | this.state = { 168 | nowShowing: ALL_TODOS, 169 | editing: null, 170 | newTodo: "", 171 | }; 172 | } 173 | 174 | componentDidMount() {} 175 | 176 | handleChange(event) { 177 | this.setState({ newTodo: event.target.value }); 178 | } 179 | 180 | handleNewTodoKeyDown(event) { 181 | if (event.keyCode !== ENTER_KEY) { 182 | return; 183 | } 184 | 185 | event.preventDefault(); 186 | 187 | var val = this.state.newTodo.trim(); 188 | 189 | if (val) { 190 | this.props.model.addTodo(val); 191 | if (this.props.onNewTodo) { 192 | this.props.onNewTodo(val); 193 | } 194 | this.setState({ newTodo: "" }); 195 | } 196 | } 197 | 198 | toggleAll(event) { 199 | var checked = event.target.checked; 200 | this.props.model.toggleAll(checked); 201 | } 202 | 203 | toggle(todoToToggle) { 204 | this.props.model.toggle(todoToToggle); 205 | } 206 | 207 | destroy(todo) { 208 | this.props.model.destroy(todo); 209 | } 210 | 211 | edit(todo) { 212 | this.setState({ editing: todo.id }); 213 | } 214 | 215 | save(todoToSave, text) { 216 | this.props.model.save(todoToSave, text); 217 | this.setState({ editing: null }); 218 | } 219 | 220 | cancel() { 221 | this.setState({ editing: null }); 222 | } 223 | 224 | clearCompleted() { 225 | this.props.model.clearCompleted(); 226 | } 227 | 228 | render() { 229 | var footer; 230 | var main; 231 | var todos = this.props.model.todos; 232 | 233 | var shownTodos = todos.filter(function (todo) { 234 | switch (this.state.nowShowing) { 235 | case ACTIVE_TODOS: 236 | return !todo.completed; 237 | case COMPLETED_TODOS: 238 | return todo.completed; 239 | default: 240 | return true; 241 | } 242 | }, this); 243 | 244 | var todoItems = shownTodos.map(function (todo) { 245 | return ( 246 | 256 | ); 257 | }, this); 258 | 259 | var activeTodoCount = todos.reduce(function (accum, todo) { 260 | return todo.completed ? accum : accum + 1; 261 | }, 0); 262 | 263 | var completedCount = todos.length - activeTodoCount; 264 | 265 | if (activeTodoCount || completedCount) { 266 | footer = ( 267 | 273 | ); 274 | } 275 | 276 | if (todos.length) { 277 | main = ( 278 |
    279 | 286 |
    289 | ); 290 | } 291 | 292 | return ( 293 |
    294 |
    295 | 303 |
    304 | {main} 305 | {footer} 306 |
    307 | ); 308 | } 309 | } 310 | 311 | export default TodoApp; 312 | -------------------------------------------------------------------------------- /components/TodoModel.js: -------------------------------------------------------------------------------- 1 | export const Utils = { 2 | uuid: function () { 3 | /*jshint bitwise:false */ 4 | var i, random; 5 | var uuid = ""; 6 | 7 | for (i = 0; i < 32; i++) { 8 | random = (Math.random() * 16) | 0; 9 | if (i === 8 || i === 12 || i === 16 || i === 20) { 10 | uuid += "-"; 11 | } 12 | uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16); 13 | } 14 | 15 | return uuid; 16 | }, 17 | 18 | pluralize: function (count, word) { 19 | return count === 1 ? word : word + "s"; 20 | }, 21 | 22 | store: function (namespace, data) { 23 | if (typeof localStorage === "undefined") { 24 | return; 25 | } 26 | if (data) { 27 | return localStorage.setItem(namespace, JSON.stringify(data)); 28 | } 29 | 30 | var store = localStorage.getItem(namespace); 31 | return (store && JSON.parse(store)) || []; 32 | }, 33 | 34 | extend: function () { 35 | var newObj = {}; 36 | for (var i = 0; i < arguments.length; i++) { 37 | var obj = arguments[i]; 38 | for (var key in obj) { 39 | if (obj.hasOwnProperty(key)) { 40 | newObj[key] = obj[key]; 41 | } 42 | } 43 | } 44 | return newObj; 45 | }, 46 | }; 47 | 48 | // Generic "model" object. You can use whatever 49 | // framework you want. For this application it 50 | // may not even be worth separating this logic 51 | // out, but we do this to demonstrate one way to 52 | // separate out parts of your application. 53 | const TodoModel = function (key) { 54 | this.key = key; 55 | this.todos = Utils.store(key) || []; 56 | this.onChanges = []; 57 | }; 58 | 59 | TodoModel.prototype.subscribe = function (onChange) { 60 | this.onChanges.push(onChange); 61 | }; 62 | 63 | TodoModel.prototype.inform = function () { 64 | Utils.store(this.key, this.todos); 65 | this.onChanges.forEach(function (cb) { 66 | cb(); 67 | }); 68 | }; 69 | 70 | TodoModel.prototype.addTodo = function (title) { 71 | this.todos = this.todos.concat({ 72 | id: Utils.uuid(), 73 | title: title, 74 | completed: false, 75 | }); 76 | 77 | this.inform(); 78 | }; 79 | 80 | TodoModel.prototype.toggleAll = function (checked) { 81 | // Note: it's usually better to use immutable data structures since they're 82 | // easier to reason about and React works very well with them. That's why 83 | // we use map() and filter() everywhere instead of mutating the array or 84 | // todo items themselves. 85 | this.todos = this.todos.map(function (todo) { 86 | return Utils.extend({}, todo, { completed: checked }); 87 | }); 88 | 89 | this.inform(); 90 | }; 91 | 92 | TodoModel.prototype.toggle = function (todoToToggle) { 93 | this.todos = this.todos.map(function (todo) { 94 | return todo !== todoToToggle ? todo : Utils.extend({}, todo, { completed: !todo.completed }); 95 | }); 96 | 97 | this.inform(); 98 | }; 99 | 100 | TodoModel.prototype.destroy = function (todo) { 101 | this.todos = this.todos.filter(function (candidate) { 102 | return candidate !== todo; 103 | }); 104 | 105 | this.inform(); 106 | }; 107 | 108 | TodoModel.prototype.save = function (todoToSave, text) { 109 | this.todos = this.todos.map(function (todo) { 110 | return todo !== todoToSave ? todo : Utils.extend({}, todo, { title: text }); 111 | }); 112 | 113 | this.inform(); 114 | }; 115 | 116 | TodoModel.prototype.clearCompleted = function () { 117 | this.todos = this.todos.filter(function (todo) { 118 | return !todo.completed; 119 | }); 120 | 121 | this.inform(); 122 | }; 123 | 124 | export default TodoModel; 125 | -------------------------------------------------------------------------------- /hooks/useParagon.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { paragon, SDK_EVENT } from "@useparagon/connect"; 3 | 4 | if (typeof window !== "undefined") { 5 | window.paragon = paragon; 6 | } 7 | 8 | export default function useParagon(paragonUserToken) { 9 | const [user, setUser] = useState(paragon.getUser()); 10 | const [error, setError] = useState(); 11 | 12 | const updateUser = useCallback(() => { 13 | const authedUser = paragon.getUser(); 14 | if (authedUser.authenticated) { 15 | setUser({ ...authedUser }); 16 | } 17 | }, []); 18 | 19 | // Listen for account state changes 20 | useEffect(() => { 21 | paragon.subscribe(SDK_EVENT.ON_INTEGRATION_INSTALL, updateUser); 22 | paragon.subscribe("onIntegrationUninstall", updateUser); 23 | return () => { 24 | paragon.unsubscribe("onIntegrationInstall", updateUser); 25 | paragon.unsubscribe("onIntegrationUninstall", updateUser); 26 | }; 27 | }, []); 28 | 29 | useEffect(() => { 30 | if (!error) { 31 | paragon 32 | .authenticate( 33 | process.env.NEXT_PUBLIC_PARAGON_PROJECT_ID, 34 | paragonUserToken 35 | ) 36 | .then(() => { 37 | const authedUser = paragon.getUser(); 38 | if (authedUser.authenticated) { 39 | setUser(authedUser); 40 | } 41 | }) 42 | .catch(setError); 43 | } 44 | }, [error, paragonUserToken]); 45 | 46 | return { 47 | paragon, 48 | user, 49 | error, 50 | updateUser, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | redirects() { 3 | return [ 4 | { 5 | source: "/", 6 | destination: "/tasks", 7 | permanent: true, 8 | }, 9 | ]; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paragon-demo", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@next/env": { 8 | "version": "12.1.5", 9 | "resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.5.tgz", 10 | "integrity": "sha512-+34yUJslfJi7Lyx6ELuN8nWcOzi27izfYnZIC1Dqv7kmmfiBVxgzR3BXhlvEMTKC2IRJhXVs2FkMY+buQe3k7Q==" 11 | }, 12 | "@next/swc-android-arm-eabi": { 13 | "version": "12.1.5", 14 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.5.tgz", 15 | "integrity": "sha512-SKnGTdYcoN04Y2DvE0/Y7/MjkA+ltsmbuH/y/hR7Ob7tsj+8ZdOYuk+YvW1B8dY20nDPHP58XgDTSm2nA8BzzA==", 16 | "optional": true 17 | }, 18 | "@next/swc-android-arm64": { 19 | "version": "12.1.5", 20 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.5.tgz", 21 | "integrity": "sha512-YXiqgQ/9Rxg1dXp6brXbeQM1JDx9SwUY/36JiE+36FXqYEmDYbxld9qkX6GEzkc5rbwJ+RCitargnzEtwGW0mw==", 22 | "optional": true 23 | }, 24 | "@next/swc-darwin-arm64": { 25 | "version": "12.1.5", 26 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.5.tgz", 27 | "integrity": "sha512-y8mhldb/WFZ6lFeowkGfi0cO/lBdiBqDk4T4LZLvCpoQp4Or/NzUN6P5NzBQZ5/b4oUHM/wQICEM+1wKA4qIVw==", 28 | "optional": true 29 | }, 30 | "@next/swc-darwin-x64": { 31 | "version": "12.1.5", 32 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.5.tgz", 33 | "integrity": "sha512-wqJ3X7WQdTwSGi0kIDEmzw34QHISRIQ5uvC+VXmsIlCPFcMA+zM5723uh8NfuKGquDMiEMS31a83QgkuHMYbwQ==", 34 | "optional": true 35 | }, 36 | "@next/swc-linux-arm-gnueabihf": { 37 | "version": "12.1.5", 38 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.5.tgz", 39 | "integrity": "sha512-WnhdM5duONMvt2CncAl+9pim0wBxDS2lHoo7ub/o/i1bRbs11UTzosKzEXVaTDCUkCX2c32lIDi1WcN2ZPkcdw==", 40 | "optional": true 41 | }, 42 | "@next/swc-linux-arm64-gnu": { 43 | "version": "12.1.5", 44 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.5.tgz", 45 | "integrity": "sha512-Jq2H68yQ4bLUhR/XQnbw3LDW0GMQn355qx6rU36BthDLeGue7YV7MqNPa8GKvrpPocEMW77nWx/1yI6w6J07gw==", 46 | "optional": true 47 | }, 48 | "@next/swc-linux-arm64-musl": { 49 | "version": "12.1.5", 50 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.5.tgz", 51 | "integrity": "sha512-KgPjwdbhDqXI7ghNN8V/WAiLquc9Ebe8KBrNNEL0NQr+yd9CyKJ6KqjayVkmX+hbHzbyvbui/5wh/p3CZQ9xcQ==", 52 | "optional": true 53 | }, 54 | "@next/swc-linux-x64-gnu": { 55 | "version": "12.1.5", 56 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.5.tgz", 57 | "integrity": "sha512-O2ErUTvCJ6DkNTSr9pbu1n3tcqykqE/ebty1rwClzIYdOgpB3T2MfEPP+K7GhUR87wmN/hlihO9ch7qpVFDGKw==", 58 | "optional": true 59 | }, 60 | "@next/swc-linux-x64-musl": { 61 | "version": "12.1.5", 62 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.5.tgz", 63 | "integrity": "sha512-1eIlZmlO/VRjxxzUBcVosf54AFU3ltAzHi+BJA+9U/lPxCYIsT+R4uO3QksRzRjKWhVQMRjEnlXyyq5SKJm7BA==", 64 | "optional": true 65 | }, 66 | "@next/swc-win32-arm64-msvc": { 67 | "version": "12.1.5", 68 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.5.tgz", 69 | "integrity": "sha512-oromsfokbEuVb0CBLLE7R9qX3KGXucZpsojLpzUh1QJjuy1QkrPJncwr8xmWQnwgtQ6ecMWXgXPB+qtvizT9Tw==", 70 | "optional": true 71 | }, 72 | "@next/swc-win32-ia32-msvc": { 73 | "version": "12.1.5", 74 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.5.tgz", 75 | "integrity": "sha512-a/51L5KzBpeZSW9LbekMo3I3Cwul+V+QKwbEIMA+Qwb2qrlcn1L9h3lt8cHqNTFt2y72ce6aTwDTw1lyi5oIRA==", 76 | "optional": true 77 | }, 78 | "@next/swc-win32-x64-msvc": { 79 | "version": "12.1.5", 80 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.5.tgz", 81 | "integrity": "sha512-/SoXW1Ntpmpw3AXAzfDRaQidnd8kbZ2oSni8u5z0yw6t4RwJvmdZy1eOaAADRThWKV+2oU90++LSnXJIwBRWYQ==", 82 | "optional": true 83 | }, 84 | "@useparagon/connect": { 85 | "version": "1.0.3", 86 | "resolved": "https://registry.npmjs.org/@useparagon/connect/-/connect-1.0.3.tgz", 87 | "integrity": "sha512-TJrpPaRZCCNDT9fTG1xTbIhJAyuVyy2bn+vxOSOFrutPUNwAd39/T6ENGViH3aYe77rPWUECVnJdNeWZH7XA/Q==", 88 | "requires": { 89 | "hash.js": "^1.1.7", 90 | "jwt-decode": "^3.1.2", 91 | "react": "^17.0.2", 92 | "tslib": "2.3.1" 93 | }, 94 | "dependencies": { 95 | "react": { 96 | "version": "17.0.2", 97 | "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", 98 | "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", 99 | "requires": { 100 | "loose-envify": "^1.1.0", 101 | "object-assign": "^4.1.1" 102 | } 103 | } 104 | } 105 | }, 106 | "buffer-equal-constant-time": { 107 | "version": "1.0.1", 108 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 109 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 110 | }, 111 | "caniuse-lite": { 112 | "version": "1.0.30001481", 113 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", 114 | "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==" 115 | }, 116 | "classnames": { 117 | "version": "2.2.6", 118 | "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", 119 | "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" 120 | }, 121 | "ecdsa-sig-formatter": { 122 | "version": "1.0.11", 123 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 124 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 125 | "requires": { 126 | "safe-buffer": "^5.0.1" 127 | } 128 | }, 129 | "hash.js": { 130 | "version": "1.1.7", 131 | "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", 132 | "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", 133 | "requires": { 134 | "inherits": "^2.0.3", 135 | "minimalistic-assert": "^1.0.1" 136 | } 137 | }, 138 | "inherits": { 139 | "version": "2.0.4", 140 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 141 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 142 | }, 143 | "js-tokens": { 144 | "version": "4.0.0", 145 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 146 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 147 | }, 148 | "jsonwebtoken": { 149 | "version": "8.5.1", 150 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 151 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 152 | "requires": { 153 | "jws": "^3.2.2", 154 | "lodash.includes": "^4.3.0", 155 | "lodash.isboolean": "^3.0.3", 156 | "lodash.isinteger": "^4.0.4", 157 | "lodash.isnumber": "^3.0.3", 158 | "lodash.isplainobject": "^4.0.6", 159 | "lodash.isstring": "^4.0.1", 160 | "lodash.once": "^4.0.0", 161 | "ms": "^2.1.1", 162 | "semver": "^5.6.0" 163 | } 164 | }, 165 | "jwa": { 166 | "version": "1.4.1", 167 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 168 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 169 | "requires": { 170 | "buffer-equal-constant-time": "1.0.1", 171 | "ecdsa-sig-formatter": "1.0.11", 172 | "safe-buffer": "^5.0.1" 173 | } 174 | }, 175 | "jws": { 176 | "version": "3.2.2", 177 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 178 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 179 | "requires": { 180 | "jwa": "^1.4.1", 181 | "safe-buffer": "^5.0.1" 182 | } 183 | }, 184 | "jwt-decode": { 185 | "version": "3.1.2", 186 | "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", 187 | "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" 188 | }, 189 | "lodash.includes": { 190 | "version": "4.3.0", 191 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 192 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" 193 | }, 194 | "lodash.isboolean": { 195 | "version": "3.0.3", 196 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 197 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" 198 | }, 199 | "lodash.isinteger": { 200 | "version": "4.0.4", 201 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 202 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" 203 | }, 204 | "lodash.isnumber": { 205 | "version": "3.0.3", 206 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 207 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" 208 | }, 209 | "lodash.isplainobject": { 210 | "version": "4.0.6", 211 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 212 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" 213 | }, 214 | "lodash.isstring": { 215 | "version": "4.0.1", 216 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 217 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" 218 | }, 219 | "lodash.once": { 220 | "version": "4.1.1", 221 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 222 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" 223 | }, 224 | "loose-envify": { 225 | "version": "1.4.0", 226 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 227 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 228 | "requires": { 229 | "js-tokens": "^3.0.0 || ^4.0.0" 230 | } 231 | }, 232 | "minimalistic-assert": { 233 | "version": "1.0.1", 234 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 235 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" 236 | }, 237 | "ms": { 238 | "version": "2.1.3", 239 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 240 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 241 | }, 242 | "nanoid": { 243 | "version": "3.3.2", 244 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", 245 | "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==" 246 | }, 247 | "next": { 248 | "version": "12.1.5", 249 | "resolved": "https://registry.npmjs.org/next/-/next-12.1.5.tgz", 250 | "integrity": "sha512-YGHDpyfgCfnT5GZObsKepmRnne7Kzp7nGrac07dikhutWQug7hHg85/+sPJ4ZW5Q2pDkb+n0FnmLkmd44htIJQ==", 251 | "requires": { 252 | "@next/env": "12.1.5", 253 | "@next/swc-android-arm-eabi": "12.1.5", 254 | "@next/swc-android-arm64": "12.1.5", 255 | "@next/swc-darwin-arm64": "12.1.5", 256 | "@next/swc-darwin-x64": "12.1.5", 257 | "@next/swc-linux-arm-gnueabihf": "12.1.5", 258 | "@next/swc-linux-arm64-gnu": "12.1.5", 259 | "@next/swc-linux-arm64-musl": "12.1.5", 260 | "@next/swc-linux-x64-gnu": "12.1.5", 261 | "@next/swc-linux-x64-musl": "12.1.5", 262 | "@next/swc-win32-arm64-msvc": "12.1.5", 263 | "@next/swc-win32-ia32-msvc": "12.1.5", 264 | "@next/swc-win32-x64-msvc": "12.1.5", 265 | "caniuse-lite": "^1.0.30001283", 266 | "postcss": "8.4.5", 267 | "styled-jsx": "5.0.1" 268 | } 269 | }, 270 | "object-assign": { 271 | "version": "4.1.1", 272 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 273 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 274 | }, 275 | "picocolors": { 276 | "version": "1.0.0", 277 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 278 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 279 | }, 280 | "postcss": { 281 | "version": "8.4.5", 282 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", 283 | "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", 284 | "requires": { 285 | "nanoid": "^3.1.30", 286 | "picocolors": "^1.0.0", 287 | "source-map-js": "^1.0.1" 288 | } 289 | }, 290 | "react": { 291 | "version": "18.0.0", 292 | "resolved": "https://registry.npmjs.org/react/-/react-18.0.0.tgz", 293 | "integrity": "sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==", 294 | "requires": { 295 | "loose-envify": "^1.1.0" 296 | } 297 | }, 298 | "react-dom": { 299 | "version": "18.0.0", 300 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.0.0.tgz", 301 | "integrity": "sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw==", 302 | "requires": { 303 | "loose-envify": "^1.1.0", 304 | "scheduler": "^0.21.0" 305 | } 306 | }, 307 | "safe-buffer": { 308 | "version": "5.2.1", 309 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 310 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 311 | }, 312 | "scheduler": { 313 | "version": "0.21.0", 314 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", 315 | "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", 316 | "requires": { 317 | "loose-envify": "^1.1.0" 318 | } 319 | }, 320 | "semver": { 321 | "version": "5.7.1", 322 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 323 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 324 | }, 325 | "source-map-js": { 326 | "version": "1.0.2", 327 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 328 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" 329 | }, 330 | "styled-jsx": { 331 | "version": "5.0.1", 332 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.1.tgz", 333 | "integrity": "sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==" 334 | }, 335 | "todomvc-app-css": { 336 | "version": "2.3.0", 337 | "resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.3.0.tgz", 338 | "integrity": "sha512-RG8hxqoVn8Be3wxyuyHfOSAXiY1Z0N+PYQOe/jxzy3wpU1Obfwd1RF1i/fz/fR+MrYL+Q+BdrUt8SsXdtR7Oow==" 339 | }, 340 | "todomvc-common": { 341 | "version": "1.0.5", 342 | "resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.5.tgz", 343 | "integrity": "sha512-D8kEJmxVMQIWwztEdH+WeiAfXRbbSCpgXq4NkYi+gduJ2tr8CNq7sYLfJvjpQ10KD9QxJwig57rvMbV2QAESwQ==" 344 | }, 345 | "tslib": { 346 | "version": "2.3.1", 347 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 348 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paragon-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node server.js", 7 | "build": "next build", 8 | "server": "NODE_ENV=production node server.js" 9 | }, 10 | "dependencies": { 11 | "@useparagon/connect": "^1.0.3", 12 | "classnames": "^2.2.6", 13 | "jsonwebtoken": "^8.5.1", 14 | "next": "^12.1.5", 15 | "react": "^18.0.0", 16 | "react-dom": "^18.0.0", 17 | "todomvc-app-css": "^2.3.0", 18 | "todomvc-common": "^1.0.5" 19 | }, 20 | "engines": { 21 | "node": "16.x" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import App from "next/app"; 2 | import "todomvc-common/base.css"; 3 | import "todomvc-app-css/index.css"; 4 | import "../styles/globals.css"; 5 | 6 | function MyApp({ Component, pageProps, user }) { 7 | // Pass the Paragon User Token as a prop to each page, from the authenticated 8 | // user object, if available 9 | return ( 10 | 15 | ); 16 | } 17 | 18 | MyApp.getInitialProps = async (appContext) => { 19 | const appProps = await App.getInitialProps(appContext); 20 | if (!appContext.ctx.req) { 21 | return appProps; 22 | } 23 | 24 | const authenticatedUser = appContext.ctx.req?.user; 25 | if (authenticatedUser) { 26 | return { 27 | ...appProps, 28 | user: authenticatedUser, 29 | }; 30 | } 31 | return appProps; 32 | }; 33 | 34 | export default MyApp; 35 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class RootDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | TaskLab 9 | 10 | 11 |
    12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | 19 | export default RootDocument; 20 | -------------------------------------------------------------------------------- /pages/integrations.js: -------------------------------------------------------------------------------- 1 | import { paragon } from '@useparagon/connect'; 2 | import Layout from "../components/Layout"; 3 | import useParagon from "../hooks/useParagon"; 4 | import styles from "../styles/Integrations.module.css"; 5 | 6 | export default function Integrations({ paragonUserToken }) { 7 | const { user } = useParagon(paragonUserToken); 8 | 9 | return ( 10 | 11 |
    12 | {paragon.getIntegrationMetadata().map((integration) => { 13 | // Check the user state if this integration is enabled for the user 14 | const integrationEnabled = 15 | user.authenticated && user.integrations[integration.type]?.enabled; 16 | 17 | return ( 18 |
    19 | 20 |

    {integration.name}

    21 | 22 | {/* When clicked, display the Connect Portal for this integration */} 23 | 26 |
    27 | ); 28 | })} 29 |
    30 |
    31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /pages/tasks.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import Layout from "../components/Layout"; 3 | import TodoApp from "../components/TodoApp"; 4 | import TodoModel from "../components/TodoModel"; 5 | import useParagon from "../hooks/useParagon"; 6 | 7 | export default function Home({ user, paragonUserToken }) { 8 | const { paragon } = useParagon(paragonUserToken); 9 | const model = useRef(new TodoModel("react-todos")); 10 | const [tick, setTick] = useState(0); 11 | useEffect(() => { 12 | model.current.subscribe(() => { 13 | setTick((prev) => prev + 1); 14 | }); 15 | }, []); 16 | const [isSSR, setIsSSR] = useState(true); 17 | useEffect(() => { 18 | setIsSSR(false); 19 | }, []); 20 | 21 | return ( 22 | 23 |
    24 | {!isSSR && ( 25 | { 28 | // When a new todo gets added, send the Task Created App Event 29 | // to Paragon. 30 | paragon.event("Task Created", { 31 | creator: user?.name, 32 | summary: newTodo, 33 | priority: "Medium", 34 | status: "Not Started", 35 | }); 36 | }} 37 | /> 38 | )} 39 |
    40 |
    41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/useparagon/paragon-connect-nextjs-example/394c88adb14b2141fdfc310be134703ddc196245/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require("http"); 2 | const { parse } = require("url"); 3 | const next = require("next"); 4 | const jsonwebtoken = require("jsonwebtoken"); 5 | 6 | const dev = process.env.NODE_ENV !== "production"; 7 | const app = next({ dev }); 8 | const handle = app.getRequestHandler(); 9 | 10 | // Generate a Paragon User Token with a JWT library, using a user ID and the 11 | // Paragon Signing Key (stored in environment variables). 12 | function generateParagonUserToken(userId) { 13 | const createdAt = Math.floor(Date.now() / 1000); 14 | return jsonwebtoken.sign( 15 | { 16 | sub: userId, 17 | iat: createdAt, 18 | exp: createdAt + 60 * 60, 19 | }, 20 | process.env.PARAGON_SIGNING_KEY, 21 | { algorithm: "RS256" } 22 | ); 23 | } 24 | 25 | // This function might use a session identifier to find a logged-in user and 26 | // return their user details. Here, we use a static value for the demo. 27 | function getLoggedInUser() { 28 | const user = { 29 | id: "1f45e694-977a-474c-b630-da5c7839ad94", 30 | name: "Sean Victory", 31 | }; 32 | user.paragonUserToken = generateParagonUserToken(user.id); 33 | return user; 34 | } 35 | 36 | const port = process.env.PORT ?? 3000; 37 | app.prepare().then(() => { 38 | createServer((req, res) => { 39 | const parsedUrl = parse(req.url, true); 40 | req.user = getLoggedInUser(); 41 | handle(req, res, parsedUrl); 42 | }).listen(port, (err) => { 43 | if (err) throw err; 44 | console.log(`> Ready on http://localhost:${port}`); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/useparagon/paragon-connect-nextjs-example/394c88adb14b2141fdfc310be134703ddc196245/styles/Home.module.css -------------------------------------------------------------------------------- /styles/Integrations.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 600px; 3 | margin-top: 90px; 4 | border-radius: 3px; 5 | border: 1px solid #e2ebf8; 6 | margin-left: auto; 7 | margin-right: auto; 8 | padding: 50px 40px; 9 | padding-bottom: 20px; 10 | background-color: white; 11 | } 12 | 13 | .row { 14 | display: flex; 15 | align-items: center; 16 | margin-bottom: 20px; 17 | } 18 | 19 | .row img { 20 | margin-right: 15px; 21 | max-height: 30px; 22 | } 23 | 24 | .row p { 25 | color: #333333; 26 | font-weight: 600; 27 | flex: 1; 28 | } 29 | 30 | .row button { 31 | border: 1px solid #e2ebf8; 32 | border-radius: 3px; 33 | box-shadow: 0px 1px 4px rgba(226, 235, 248, 0.7); 34 | color: #333333; 35 | padding: 8px 24px; 36 | font-weight: 600; 37 | width: 100px; 38 | } 39 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, 6 | Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | max-width: none !important; 8 | } 9 | 10 | section.todoapp { 11 | max-width: 550px; 12 | margin-left: auto; 13 | margin-right: auto; 14 | } 15 | --------------------------------------------------------------------------------