├── .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 | [](https://heroku.com/deploy?template=https://github.com/useparagon/paragon-connect-nextjs-example)
4 | [](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 |
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 | {this.props.todo.title}
93 |
94 |
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 |
119 | Clear completed
120 |
121 | );
122 | }
123 |
124 | var nowShowing = this.props.nowShowing;
125 | return (
126 |
127 |
128 | {this.props.count} {activeTodoWord} left
129 |
130 |
158 | {clearButton}
159 |
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 |
289 | );
290 | }
291 |
292 | return (
293 |
294 |
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 |
paragon.connect(integration.type)}>
24 | {integrationEnabled ? "Manage" : "Enable"}
25 |
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 |
--------------------------------------------------------------------------------