├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── components ├── .keep ├── WalletCard.tsx ├── WalletList.tsx ├── formatted-date.tsx ├── graphics │ ├── login.tsx │ └── portal.tsx ├── layout.tsx ├── logo.tsx └── navbar.tsx ├── extensions.json ├── lib └── utils.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ ├── ethereum │ │ └── personal_sign.ts │ ├── solana │ │ └── sign_message.ts │ └── verify.ts ├── dashboard.tsx └── index.tsx ├── postcss.config.js ├── public ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ ├── icon.svg │ └── manifest.json ├── fonts │ ├── AdelleSans-Regular.woff │ ├── AdelleSans-Regular.woff2 │ ├── AdelleSans-Semibold.woff │ └── AdelleSans-Semibold.woff2 ├── images │ └── avatar.png └── logos │ ├── privy-logo.png │ └── privy-logomark.png ├── renovate.json ├── styles └── globals.css ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_PRIVY_APP_ID= 2 | PRIVY_APP_SECRET= 3 | # Session signer ID and secret are only needed for using session signers 4 | # You can create these values in the Privy dashboard as described here https://docs.privy.io/wallets/using-wallets/session-signers/configure-session-signers 5 | NEXT_PUBLIC_SESSION_SIGNER_ID= 6 | SESSION_SIGNER_SECRET= 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | .env.local 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Privy 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Privy Auth `create-next-app` Starter 2 | 3 | This is a template for integrating [**Privy Auth**](https://www.privy.io/) into a [NextJS](https://nextjs.org/) project. Check out the deployed app [here](https://create-next-app.privy.io/)! 4 | 5 | This demo uses NextJS's [Pages Router](https://nextjs.org/docs/pages/building-your-application/routing). If you'd like to see an example using the [App Router](https://nextjs.org/docs/app), just change the branch of this repository to [`app-router`](https://github.com/privy-io/create-next-app/tree/app-router). 6 | 7 | ## Setup 8 | 9 | 1. Clone this repository and open it in your terminal. 10 | ```sh 11 | git clone https://github.com/privy-io/create-next-app 12 | ``` 13 | 14 | 2. Install the necessary dependencies (including [Privy Auth](https://www.npmjs.com/package/@privy-io/react-auth)) with `npm`. 15 | ```sh 16 | npm i 17 | ``` 18 | 19 | 3. Initialize your environment variables by copying the `.env.example` file to an `.env.local` file. Then, in `.env.local`, [paste your Privy App ID from the dashboard](https://docs.privy.io/guide/dashboard/api-keys). 20 | ```sh 21 | # In your terminal, create .env.local from .env.example 22 | cp .env.example .env.local 23 | 24 | # Add your Privy App ID to .env.local 25 | NEXT_PUBLIC_PRIVY_APP_ID= 26 | ``` 27 | 28 | ## Building locally 29 | 30 | In your project directory, run `npm run dev`. You can now visit http://localhost:3000 to see your app and login with Privy! 31 | 32 | 33 | ## Check out: 34 | - `pages/_app.tsx` for how to use the `PrivyProvider` and initialize it with your Privy App ID 35 | - `pages/index.tsx` for how to use the `usePrivy` hook and implement a simple `login` button 36 | - `pages/dashboard.tsx` for how to use the `usePrivy` hook, fields like `ready`, `authenticated`, and `user`, and methods like `linkWallet` and `logout` 37 | 38 | 39 | **Check out [our docs](https://docs.privy.io/) for more guidance around using Privy in your app!** 40 | -------------------------------------------------------------------------------- /components/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/components/.keep -------------------------------------------------------------------------------- /components/WalletCard.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { 3 | getAccessToken, 4 | useSessionSigners, 5 | useSignMessage, 6 | useSignMessage as useSignMessageSolana, 7 | WalletWithMetadata, 8 | } from "@privy-io/react-auth"; 9 | import axios from "axios"; 10 | 11 | const SESSION_SIGNER_ID = process.env.NEXT_PUBLIC_SESSION_SIGNER_ID; 12 | 13 | interface WalletCardProps { 14 | wallet: WalletWithMetadata; 15 | } 16 | 17 | export default function WalletCard({ wallet }: WalletCardProps) { 18 | const { addSessionSigners, removeSessionSigners } = useSessionSigners(); 19 | const { signMessage: signMessageEthereum } = useSignMessage(); 20 | const { signMessage: signMessageSolana } = useSignMessageSolana(); 21 | const [isLoading, setIsLoading] = useState(false); 22 | const [isRemoteSigning, setIsRemoteSigning] = useState(false); 23 | const [isClientSigning, setIsClientSigning] = useState(false); 24 | 25 | // Check if this specific wallet has session signers 26 | const hasSessionSigners = wallet.delegated === true; 27 | 28 | const addSessionSigner = useCallback( 29 | async (walletAddress: string) => { 30 | if (!SESSION_SIGNER_ID) { 31 | console.error("SESSION_SIGNER_ID must be defined to addSessionSigner"); 32 | return; 33 | } 34 | 35 | setIsLoading(true); 36 | try { 37 | await addSessionSigners({ 38 | address: walletAddress, 39 | signers: [ 40 | { 41 | signerId: SESSION_SIGNER_ID, 42 | // This is a placeholder - in a real app, you would use a policy ID from your Privy dashboard 43 | policyIds: [], 44 | }, 45 | ], 46 | }); 47 | } catch (error) { 48 | console.error("Error adding session signer:", error); 49 | } finally { 50 | setIsLoading(false); 51 | } 52 | }, 53 | [addSessionSigners] 54 | ); 55 | 56 | const removeSessionSigner = useCallback( 57 | async (walletAddress: string) => { 58 | setIsLoading(true); 59 | try { 60 | await removeSessionSigners({ address: walletAddress }); 61 | } catch (error) { 62 | console.error("Error removing session signer:", error); 63 | } finally { 64 | setIsLoading(false); 65 | } 66 | }, 67 | [removeSessionSigners] 68 | ); 69 | 70 | const handleClientSign = useCallback(async () => { 71 | setIsClientSigning(true); 72 | try { 73 | const message = `Signing this message to verify ownership of ${wallet.address}`; 74 | let signature; 75 | if (wallet.chainType === "ethereum") { 76 | const result = await signMessageEthereum({ message }); 77 | signature = result.signature; 78 | } else if (wallet.chainType === "solana") { 79 | const result = await signMessageSolana({ 80 | message, 81 | }); 82 | signature = result.signature; 83 | } 84 | console.log("Message signed on client! Signature: ", signature); 85 | } catch (error) { 86 | console.error("Error signing message:", error); 87 | } finally { 88 | setIsClientSigning(false); 89 | } 90 | }, [wallet]); 91 | 92 | const handleRemoteSign = useCallback(async () => { 93 | setIsRemoteSigning(true); 94 | try { 95 | const authToken = await getAccessToken(); 96 | const path = 97 | wallet.chainType === "ethereum" 98 | ? "/api/ethereum/personal_sign" 99 | : "/api/solana/sign_message"; 100 | const message = `Signing this message to verify ownership of ${wallet.address}`; 101 | const response = await axios.post( 102 | path, 103 | { 104 | wallet_id: wallet.id, 105 | message: message, 106 | }, 107 | { 108 | headers: { 109 | Authorization: `Bearer ${authToken}`, 110 | }, 111 | } 112 | ); 113 | 114 | const data = response.data; 115 | 116 | if (response.status === 200) { 117 | console.log( 118 | "Message signed on server! Signature: " + data.data.signature 119 | ); 120 | } else { 121 | throw new Error(data.error || "Failed to sign message"); 122 | } 123 | } catch (error) { 124 | console.error("Error signing message:", error); 125 | } finally { 126 | setIsRemoteSigning(false); 127 | } 128 | }, [wallet.id]); 129 | 130 | return ( 131 |
132 |
133 | {wallet.walletClientType === "privy" ? "Embedded " : ""}Wallet:{" "} 134 | {wallet.address.slice(0, 6)}... 135 | {wallet.address.slice(-4)} 136 |
137 | 138 |
139 | 150 | 151 | 162 |
163 | 164 | {hasSessionSigners && ( 165 |
166 | This wallet has active session signers 167 |
168 | )} 169 | 170 |
171 | 182 | 183 | 194 |
195 |
196 | ); 197 | } 198 | -------------------------------------------------------------------------------- /components/WalletList.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useCreateWallet, 3 | useSolanaWallets, 4 | WalletWithMetadata, 5 | useUser, 6 | } from "@privy-io/react-auth"; 7 | import { useCallback, useMemo, useState } from "react"; 8 | import WalletCard from "./WalletCard"; 9 | 10 | export default function WalletList() { 11 | const { user } = useUser(); 12 | const { createWallet: createEthereumWallet } = useCreateWallet(); 13 | const { createWallet: createSolanaWallet } = useSolanaWallets(); 14 | const [isCreating, setIsCreating] = useState(false); 15 | 16 | const ethereumEmbeddedWallets = useMemo( 17 | () => 18 | (user?.linkedAccounts.filter( 19 | (account) => 20 | account.type === "wallet" && 21 | account.walletClientType === "privy" && 22 | account.chainType === "ethereum" 23 | ) as WalletWithMetadata[]) ?? [], 24 | [user] 25 | ); 26 | 27 | const solanaEmbeddedWallets = useMemo( 28 | () => 29 | (user?.linkedAccounts.filter( 30 | (account) => 31 | account.type === "wallet" && 32 | account.walletClientType === "privy" && 33 | account.chainType === "solana" 34 | ) as WalletWithMetadata[]) ?? [], 35 | [user] 36 | ); 37 | 38 | const handleCreateWallet = useCallback( 39 | async (type: "ethereum" | "solana") => { 40 | setIsCreating(true); 41 | try { 42 | if (type === "ethereum") { 43 | await createEthereumWallet(); 44 | } else if (type === "solana") { 45 | await createSolanaWallet(); 46 | } 47 | } catch (error) { 48 | console.error("Error creating wallet:", error); 49 | } finally { 50 | setIsCreating(false); 51 | } 52 | }, 53 | [createEthereumWallet, createSolanaWallet] 54 | ); 55 | 56 | return ( 57 |
58 | {ethereumEmbeddedWallets.length === 0 ? ( 59 |
60 |

61 | No Ethereum embedded wallets found. 62 |

63 | 70 |
71 | ) : ( 72 |
73 | {ethereumEmbeddedWallets.map((wallet) => ( 74 | 75 | ))} 76 |
77 | )} 78 | {solanaEmbeddedWallets.length === 0 ? ( 79 |
80 |

81 | No Solana embedded wallets found. 82 |

83 | 90 |
91 | ) : ( 92 |
93 | {solanaEmbeddedWallets.map((wallet) => ( 94 | 95 | ))} 96 |
97 | )} 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /components/formatted-date.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | secsSinceEpoch: number; 3 | }; 4 | 5 | export default function FormattedDate({ secsSinceEpoch }: Props) { 6 | const formattedDate = new Date(secsSinceEpoch * 1000).toLocaleDateString( 7 | "en-us", 8 | { 9 | weekday: "long", 10 | year: "numeric", 11 | month: "short", 12 | day: "numeric", 13 | }, 14 | ); 15 | 16 | return

{formattedDate}

; 17 | } 18 | -------------------------------------------------------------------------------- /components/graphics/login.tsx: -------------------------------------------------------------------------------- 1 | export default function LoginGraphic(props: React.SVGProps) { 2 | return ( 3 | 11 | 15 | 19 | 23 | 27 | 31 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 107 | 113 | 119 | 125 | 131 | 135 | 139 | 143 | 147 | 151 | 155 | 159 | 163 | 167 | 171 | 175 | 179 | 183 | 189 | 194 | 199 | 203 | 207 | 211 | 215 | 219 | 225 | 229 | 233 | 237 | 241 | 245 | 249 | 253 | 257 | 261 | 265 | 269 | 273 | 277 | 281 | 285 | 290 | 294 | 298 | 302 | 306 | 310 | 314 | 318 | 322 | 323 | 331 | 332 | 333 | 334 | 335 | 343 | 344 | 345 | 346 | 354 | 355 | 356 | 357 | 365 | 366 | 367 | 368 | 376 | 377 | 378 | 379 | 387 | 388 | 389 | 390 | 398 | 399 | 400 | 401 | 409 | 410 | 411 | 412 | 413 | 421 | 422 | 423 | 424 | 425 | 433 | 434 | 435 | 436 | 437 | 445 | 446 | 447 | 448 | 456 | 457 | 458 | 459 | 467 | 468 | 469 | 470 | 478 | 479 | 480 | 481 | 489 | 490 | 491 | 492 | 500 | 501 | 502 | 503 | 511 | 512 | 513 | 514 | 522 | 523 | 524 | 525 | 533 | 534 | 535 | 536 | 544 | 545 | 546 | 547 | 555 | 556 | 557 | 558 | 566 | 567 | 568 | 569 | 577 | 578 | 579 | 580 | 588 | 589 | 590 | 591 | 599 | 600 | 601 | 602 | 610 | 611 | 612 | 613 | 621 | 622 | 623 | 624 | 632 | 633 | 634 | 635 | 643 | 644 | 645 | 646 | 654 | 655 | 656 | 657 | 665 | 666 | 667 | 668 | 676 | 677 | 678 | 679 | 687 | 688 | 689 | 690 | 698 | 699 | 700 | 701 | 709 | 710 | 711 | 712 | 720 | 721 | 722 | 723 | 731 | 732 | 733 | 734 | 742 | 743 | 744 | 745 | 753 | 754 | 755 | 756 | 764 | 765 | 766 | 767 | 775 | 776 | 777 | 778 | 786 | 787 | 788 | 789 | 797 | 798 | 799 | 800 | 808 | 809 | 810 | 811 | 819 | 820 | 821 | 822 | 830 | 831 | 832 | 833 | 841 | 842 | 843 | 844 | 852 | 853 | 854 | 855 | 863 | 864 | 865 | 866 | 874 | 875 | 876 | 877 | 885 | 886 | 887 | 888 | 896 | 897 | 898 | 899 | 907 | 908 | 909 | 910 | 911 | 912 | ); 913 | } 914 | -------------------------------------------------------------------------------- /components/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { usePrivy } from "@privy-io/react-auth"; 3 | import Navbar from "./navbar"; 4 | import type { NavbarItem } from "./navbar"; 5 | import { useRouter } from "next/router"; 6 | 7 | type Props = { 8 | children?: React.ReactNode; 9 | accountId: string; 10 | appName: string; 11 | navbarItems: Array; 12 | }; 13 | 14 | export default function Layout({ 15 | children, 16 | accountId, 17 | appName, 18 | navbarItems, 19 | }: Props) { 20 | const { ready, authenticated } = usePrivy(); 21 | const router = useRouter(); 22 | 23 | useEffect(() => { 24 | if (ready && !authenticated) { 25 | router.push("/"); 26 | } 27 | }, [ready, authenticated, router]); 28 | 29 | return ( 30 | <> 31 | 32 |
{children}
33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/logo.tsx: -------------------------------------------------------------------------------- 1 | interface LogoPropsType { 2 | fontColor?: string; 3 | width?: string; 4 | height?: string; 5 | } 6 | 7 | export function Logo(props: LogoPropsType) { 8 | const fontColor = props.fontColor || "white"; 9 | const width = props.width || "151"; 10 | const height = props.height || "44"; 11 | 12 | return ( 13 | 20 | 21 | 25 | 31 | 35 | 39 | 43 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useRouter } from "next/router"; 3 | import { Fragment } from "react"; 4 | import { Disclosure, Menu, Transition } from "@headlessui/react"; 5 | import { 6 | ChevronDownIcon, 7 | Bars3Icon, 8 | XMarkIcon, 9 | InformationCircleIcon, 10 | } from "@heroicons/react/24/outline"; 11 | import { Logo } from "./logo"; 12 | 13 | function classNames(...classes: Array): string { 14 | return classes.filter(Boolean).join(" "); 15 | } 16 | 17 | /** 18 | * make sure you are passing router.pathname and not 19 | * router.asPath since we want to have stripped any 20 | * fragments, query params, or trailing slashes 21 | */ 22 | const extractTabFromPath = (path: string) => { 23 | return path.split("/").pop() as string; 24 | }; 25 | 26 | export type NavbarItem = { 27 | id: string; 28 | name: string; 29 | resource: string; 30 | }; 31 | 32 | type NavbarProps = { 33 | accountId: string; 34 | appName: string; 35 | items: Array; 36 | }; 37 | 38 | export default function Navbar({ items, accountId, appName }: NavbarProps) { 39 | const router = useRouter(); 40 | const resourceId = router.query.id; 41 | const selected = extractTabFromPath(router.pathname); 42 | 43 | const selectedItemClass = 44 | "hover:cursor-pointer rounded-full bg-gray-900 px-3 py-2 text-lg font-medium text-white"; 45 | const unselectedItemClass = 46 | "hover:cursor-pointer rounded-full px-3 py-2 text-lg font-medium text-gray-300 hover:bg-gray-700 hover:text-white"; 47 | 48 | // Navigate to a resource sub-page: 49 | // /apps/:appId/settings 50 | // /accounts/:accountId/users 51 | const navigateTo = (item: NavbarItem) => { 52 | router.push(`/${item.resource}/${resourceId}/${item.id}`); 53 | }; 54 | 55 | return ( 56 | 57 | {({ open }) => ( 58 | <> 59 |
60 |
61 |
62 |
63 |
64 | 65 |
66 |
67 | 68 |
69 |
70 |
71 |
72 | {items ? ( 73 | items.map((item) => { 74 | return ( 75 | 88 | ); 89 | }) 90 | ) : ( 91 |
92 | )} 93 |
94 |
95 |
96 |
97 |
98 | 107 |

{appName}

108 | 109 | {/* Profile dropdown */} 110 | 111 |
112 | 113 | Open user menu 114 |
115 | avatar placeholder 122 |
123 |
124 |
129 | 138 | 139 | 140 | {({ active }) => ( 141 | 148 | Your account 149 | 150 | )} 151 | 152 | 153 | {({ active }) => ( 154 | 161 | Settings 162 | 163 | )} 164 | 165 | 166 | {({ active }) => ( 167 | 174 | Sign out 175 | 176 | )} 177 | 178 | 179 | 180 |
181 |
182 |
183 |
184 | {/* Mobile menu button */} 185 | 186 | Open main menu 187 | {open ? ( 188 | 193 |
194 |
195 |
196 | 197 | )} 198 |
199 | ); 200 | } 201 | -------------------------------------------------------------------------------- /extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "tamasfe.even-better-toml", 6 | "mikestead.dotenv", 7 | "austenc.tailwind-docs", 8 | "styled-components.vscode-styled-components" 9 | ] 10 | } -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { AuthTokenClaims, PrivyClient } from "@privy-io/server-auth"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | export type APIError = { 5 | error: string; 6 | cause?: string; 7 | }; 8 | 9 | /** 10 | * Authorizes a user to call an endpoint, returning either an error result or their verifiedClaims 11 | * @param req - The API request 12 | * @param res - The API response 13 | * @param client - A PrivyClient 14 | */ 15 | export const fetchAndVerifyAuthorization = async ( 16 | req: NextApiRequest, 17 | res: NextApiResponse, 18 | client: PrivyClient 19 | ): Promise => { 20 | const header = req.headers.authorization; 21 | if (!header) { 22 | return res.status(401).json({ error: "Missing auth token." }); 23 | } 24 | const authToken = header.replace(/^Bearer /, ""); 25 | 26 | try { 27 | return client.verifyAuthToken(authToken); 28 | } catch { 29 | return res.status(401).json({ error: "Invalid auth token." }); 30 | } 31 | }; 32 | 33 | export const createPrivyClient = () => { 34 | return new PrivyClient( 35 | process.env.NEXT_PUBLIC_PRIVY_APP_ID as string, 36 | process.env.PRIVY_APP_SECRET as string, 37 | { 38 | walletApi: { 39 | authorizationPrivateKey: process.env.SESSION_SIGNER_SECRET, 40 | }, 41 | } 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "npm": ">=9.0.0", 5 | "node": ">=18.0.0" 6 | }, 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "format": "npx prettier --write \"{__tests__,components,pages,styles}/**/*.{ts,tsx,js,jsx}\"", 12 | "lint": "next lint && npx prettier --check \"{__tests__,components,pages,styles}/**/*.{ts,tsx,js,jsx}\" && npx tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@headlessui/react": "^1.7.3", 16 | "@heroicons/react": "^2.0.12", 17 | "@privy-io/react-auth": "^2.12.0", 18 | "@privy-io/server-auth": "1.21.2", 19 | "@tailwindcss/forms": "^0.5.3", 20 | "axios": "^1.9.0", 21 | "next": "latest", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0" 24 | }, 25 | "devDependencies": { 26 | "@tsconfig/next": "^2.0.0", 27 | "@tsconfig/node18": "^18.2.0", 28 | "@tsconfig/strictest": "^2.0.1", 29 | "@types/node": "^18", 30 | "@types/react": "18.2.0", 31 | "autoprefixer": "^10.4.7", 32 | "dotenv-cli": "^6.0.0", 33 | "eslint": "^8.23.0", 34 | "eslint-config-next": "12.2.5", 35 | "postcss": "^8.4.14", 36 | "tailwindcss": "^3.1.2", 37 | "ts-node": "^10.9.1", 38 | "typescript": "^5.1.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import Head from "next/head"; 4 | import { PrivyProvider } from "@privy-io/react-auth"; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | <> 9 | 10 | 16 | 22 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Privy Auth Starter 41 | 42 | 43 | 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | export default MyApp; 58 | -------------------------------------------------------------------------------- /pages/api/ethereum/personal_sign.ts: -------------------------------------------------------------------------------- 1 | import { WalletApiRpcResponseType } from "@privy-io/public-api"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { 4 | APIError, 5 | fetchAndVerifyAuthorization, 6 | createPrivyClient, 7 | } from "../../../lib/utils"; 8 | 9 | const client = createPrivyClient(); 10 | 11 | export default async function POST( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ) { 15 | const errorOrVerifiedClaims = await fetchAndVerifyAuthorization( 16 | req, 17 | res, 18 | client 19 | ); 20 | const authorized = errorOrVerifiedClaims && "appId" in errorOrVerifiedClaims; 21 | if (!authorized) return errorOrVerifiedClaims; 22 | 23 | const message = req.body.message; 24 | const walletId = req.body.wallet_id; 25 | 26 | if (!message || !walletId) { 27 | return res 28 | .status(400) 29 | .json({ error: "Message and wallet_id are required" }); 30 | } 31 | 32 | try { 33 | const { signature } = await client.walletApi.ethereum.signMessage({ 34 | walletId, 35 | message, 36 | }); 37 | return res.status(200).json({ 38 | method: "personal_sign", 39 | data: { 40 | signature: signature, 41 | encoding: "hex", 42 | }, 43 | }); 44 | } catch (error) { 45 | console.error(error); 46 | let statusCode = 500; 47 | 48 | return res.status(statusCode).json({ 49 | error: (error as Error).message, 50 | cause: (error as Error).stack, 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pages/api/solana/sign_message.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { 3 | APIError, 4 | createPrivyClient, 5 | fetchAndVerifyAuthorization, 6 | } from "../../../lib/utils"; 7 | import { WalletApiRpcResponseType } from "@privy-io/public-api"; 8 | const client = createPrivyClient(); 9 | 10 | export default async function POST( 11 | req: NextApiRequest, 12 | res: NextApiResponse 13 | ) { 14 | const errorOrVerifiedClaims = await fetchAndVerifyAuthorization( 15 | req, 16 | res, 17 | client 18 | ); 19 | const authorized = errorOrVerifiedClaims && "appId" in errorOrVerifiedClaims; 20 | if (!authorized) return errorOrVerifiedClaims; 21 | 22 | const message = req.body.message; 23 | const walletId = req.body.wallet_id; 24 | 25 | if (!message || !walletId) { 26 | return res 27 | .status(400) 28 | .json({ error: "Message and wallet_id are required" }); 29 | } 30 | 31 | try { 32 | // Sign the message using Privy's wallet API 33 | const { signature } = await client.walletApi.solana.signMessage({ 34 | walletId, 35 | message, 36 | }); 37 | 38 | return res.status(200).json({ 39 | method: "signMessage", 40 | data: { 41 | signature: Buffer.from(signature).toString("base64"), 42 | encoding: "base64", 43 | }, 44 | }); 45 | } catch (error) { 46 | console.error("Error signing message:", error); 47 | return res.status(500).json({ 48 | error: (error as Error).message, 49 | cause: (error as Error).stack, 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pages/api/verify.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import { PrivyClient, AuthTokenClaims } from "@privy-io/server-auth"; 4 | 5 | const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID; 6 | const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET; 7 | const client = new PrivyClient(PRIVY_APP_ID!, PRIVY_APP_SECRET!); 8 | 9 | export type AuthenticateSuccessResponse = { 10 | claims: AuthTokenClaims; 11 | }; 12 | 13 | export type AuthenticationErrorResponse = { 14 | error: string; 15 | }; 16 | 17 | async function handler( 18 | req: NextApiRequest, 19 | res: NextApiResponse< 20 | AuthenticateSuccessResponse | AuthenticationErrorResponse 21 | >, 22 | ) { 23 | const headerAuthToken = req.headers.authorization?.replace(/^Bearer /, ""); 24 | const cookieAuthToken = req.cookies["privy-token"]; 25 | 26 | const authToken = cookieAuthToken || headerAuthToken; 27 | if (!authToken) return res.status(401).json({ error: "Missing auth token" }); 28 | 29 | try { 30 | const claims = await client.verifyAuthToken(authToken); 31 | return res.status(200).json({ claims }); 32 | } catch (e: any) { 33 | return res.status(401).json({ error: e.message }); 34 | } 35 | } 36 | 37 | export default handler; 38 | -------------------------------------------------------------------------------- /pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect, useState } from "react"; 3 | import { getAccessToken, usePrivy } from "@privy-io/react-auth"; 4 | import Head from "next/head"; 5 | import WalletList from "../components/WalletList"; 6 | 7 | async function verifyToken() { 8 | const url = "/api/verify"; 9 | const accessToken = await getAccessToken(); 10 | const result = await fetch(url, { 11 | headers: { 12 | ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined), 13 | }, 14 | }); 15 | 16 | return await result.json(); 17 | } 18 | 19 | export default function DashboardPage() { 20 | const [verifyResult, setVerifyResult] = useState(); 21 | const router = useRouter(); 22 | const { 23 | ready, 24 | authenticated, 25 | user, 26 | logout, 27 | linkEmail, 28 | linkWallet, 29 | unlinkEmail, 30 | linkPhone, 31 | unlinkPhone, 32 | unlinkWallet, 33 | linkGoogle, 34 | unlinkGoogle, 35 | linkTwitter, 36 | unlinkTwitter, 37 | linkDiscord, 38 | unlinkDiscord, 39 | } = usePrivy(); 40 | 41 | useEffect(() => { 42 | if (ready && !authenticated) { 43 | router.push("/"); 44 | } 45 | }, [ready, authenticated, router]); 46 | 47 | const numAccounts = user?.linkedAccounts?.length || 0; 48 | const canRemoveAccount = numAccounts > 1; 49 | 50 | const email = user?.email; 51 | const phone = user?.phone; 52 | const wallet = user?.wallet; 53 | 54 | const googleSubject = user?.google?.subject || null; 55 | const twitterSubject = user?.twitter?.subject || null; 56 | const discordSubject = user?.discord?.subject || null; 57 | 58 | return ( 59 | <> 60 | 61 | Privy Auth Demo 62 | 63 | 64 |
65 | {ready && authenticated ? ( 66 | <> 67 |
68 |

Privy Auth Demo

69 | 75 |
76 |
77 | {googleSubject ? ( 78 | 87 | ) : ( 88 | 96 | )} 97 | 98 | {twitterSubject ? ( 99 | 108 | ) : ( 109 | 117 | )} 118 | 119 | {discordSubject ? ( 120 | 129 | ) : ( 130 | 138 | )} 139 | 140 | {email ? ( 141 | 150 | ) : ( 151 | 157 | )} 158 | {wallet ? ( 159 | 168 | ) : ( 169 | 175 | )} 176 | {phone ? ( 177 | 186 | ) : ( 187 | 193 | )} 194 | 195 | 201 | 202 | {Boolean(verifyResult) && ( 203 |
204 | 205 | Server verify result 206 | 207 |
208 |                     {JSON.stringify(verifyResult, null, 2)}
209 |                   
210 |
211 | )} 212 |
213 |
214 |

Your Wallet

215 | 216 |
217 |

218 | User object 219 |

220 |
221 |               {JSON.stringify(user, null, 2)}
222 |             
223 | 224 | ) : null} 225 |
226 | 227 | ); 228 | } 229 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Portal from "../components/graphics/portal"; 2 | import { useLogin } from "@privy-io/react-auth"; 3 | import { PrivyClient } from "@privy-io/server-auth"; 4 | import { GetServerSideProps } from "next"; 5 | import Head from "next/head"; 6 | import { useRouter } from "next/router"; 7 | 8 | export const getServerSideProps: GetServerSideProps = async ({ req }) => { 9 | const cookieAuthToken = req.cookies["privy-token"]; 10 | 11 | // If no cookie is found, skip any further checks 12 | if (!cookieAuthToken) return { props: {} }; 13 | 14 | const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID; 15 | const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET; 16 | const client = new PrivyClient(PRIVY_APP_ID!, PRIVY_APP_SECRET!); 17 | 18 | try { 19 | const claims = await client.verifyAuthToken(cookieAuthToken); 20 | // Use this result to pass props to a page for server rendering or to drive redirects! 21 | // ref https://nextjs.org/docs/pages/api-reference/functions/get-server-side-props 22 | console.log({ claims }); 23 | 24 | return { 25 | props: {}, 26 | redirect: { destination: "/dashboard", permanent: false }, 27 | }; 28 | } catch (error) { 29 | return { props: {} }; 30 | } 31 | }; 32 | 33 | export default function LoginPage() { 34 | const router = useRouter(); 35 | const { login } = useLogin({ 36 | onComplete: () => router.push("/dashboard"), 37 | }); 38 | 39 | return ( 40 | <> 41 | 42 | Login · Privy 43 | 44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | 58 |
59 |
60 |
61 |
62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Privy Auth Demo", 3 | "icons": [ 4 | {"src": "/favicons/android-chrome-192x192.png", "type": "image/png", "sizes": "192x192"}, 5 | {"src": "/favicons/android-chrome-512x512.png", "type": "image/png", "sizes": "512x512"} 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /public/fonts/AdelleSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/fonts/AdelleSans-Regular.woff -------------------------------------------------------------------------------- /public/fonts/AdelleSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/fonts/AdelleSans-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/AdelleSans-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/fonts/AdelleSans-Semibold.woff -------------------------------------------------------------------------------- /public/fonts/AdelleSans-Semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/fonts/AdelleSans-Semibold.woff2 -------------------------------------------------------------------------------- /public/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/images/avatar.png -------------------------------------------------------------------------------- /public/logos/privy-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/logos/privy-logo.png -------------------------------------------------------------------------------- /public/logos/privy-logomark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privy-io/create-next-app/b3536ba5d115b46d7f7a29a5c9e71341b0714fdc/public/logos/privy-logomark.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "encrypted": { 5 | "npmrc": "wcFMA/xDdHCJBTolAQ/8DzZm/3mNUotcOA8ldKAs1JvwdF0Vfu56Z1Houknw7uTtSDZTKZDQGzAJ4Jj3JuY1gbYwxFufM27YRyZYNdi7xTZOaG79i9k3lcHhC0PHxdTkulE8uKGcePsHL3Ajg+GZ3h1pS/h0rHnCtGHT80AguSHMN/mLZ9MarV75WetpExWhALPMU2Pve4F2ELwoa1+yVZNwgsyzLOnKRbl3cS8ghVwDaHCvNiOd7IEXve+27Crq310W6Y+TO5RHTEMNpeR9BxhATUjqJemi8UEIAeh7ezGnZe6Q0m36AeeTZF3vc4pIkZUxyY60ApZVshORky/iC4KK3PVWWA4jj4ad8DgMc0KmKV91s0UmjHuYuQPQg/bb1E/S85yZiJHwQfZHWbvxAAltuntvsyZmWXR63VwQPsrWc+ecO54SXsOhhIPO5mQPbgLLJ5EARqrReYMUoI7t7wxQQ7jrc7FbEayFZ7oFreyOqRyeIwh6r+mhLaeGGXVcncUiN0GJxQzPeyJA2PCPMC/2c7qoXCXRY/lNvI2GpMSlLA4DM8/q3ZDjw0fKC/kPsJNAzduLX6T4syaqWAPB3KWfx2ck34qr8CKrp/VxM7mt1EewyfAYOfGdAwlUnn5cFiP05AzNkFA+4mwH/4aHyk8EFzdiTj59Uoinorx8NimzLkNxvUlLYhBJXBWBxqHSwHsBoLX5VLjqvhFY55/PSph1kxUc8PhQZsmu2MIubBaYBYUHt9ukejv0LK9n3V6ZBba76pv6WRiPFv7DOKH7TfqiLFu86hSt6MjeruuXyJ0CJffSDujjtdpNb8z125JPsZnXSCYqPIjIVi8wQ8gI+ENlYIaA+wgNO8hBXv4SU6HiBu6HD5zn9ueVqGTdVM1Lk2/jJpuWPcgo3fnLh5Dkw2deDOshURkiItneqjHb2o8Ojfpv/zgANhbE40p3Z2m4EehEBQ3JpIFAM43lolY8AjWQG8LkSA0W15KRBqVO7Zd9Qt1WQfKH0KNvKhjl5tIkCV3d02wZs/TiSaBDn1+CllzDT9PmzyaR17P05EZcHnrtRsTVjTu9X0VYz9p/YvWpzM1ao3b8FqvgkZKGMwHHkUocKXopLPIeq+QxT38" 6 | }, 7 | "packageRules": [ 8 | { 9 | "packagePatterns": ["*"], 10 | "excludePackagePatterns": ["@privy-io"], 11 | "enabled": false 12 | }, 13 | { 14 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 15 | "automerge": true 16 | } 17 | ], 18 | "rangeStrategy": "pin", 19 | "constraints": { 20 | "node": ">=18.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | /* normal */ 7 | @font-face { 8 | font-family: "Adelle Sans"; 9 | src: url("/fonts/AdelleSans-Regular.woff2") format("woff2"); 10 | font-style: normal; 11 | font-display: swap; 12 | font-weight: 300; 13 | } 14 | 15 | /* semibold */ 16 | @font-face { 17 | font-family: "Adelle Sans"; 18 | src: url("/fonts/AdelleSans-Semibold.woff2") format("woff2"); 19 | font-style: normal; 20 | font-display: swap; 21 | font-weight: 600; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | sans: ['Adelle Sans', ...defaultTheme.fontFamily.sans], 10 | }, 11 | colors: { 12 | 'privy-navy': '#160B45', 13 | 'privy-light-blue': '#EFF1FD', 14 | 'privy-blueish': '#D4D9FC', 15 | 'privy-pink': '#FF8271', 16 | }, 17 | }, 18 | }, 19 | plugins: [require('@tailwindcss/forms')], 20 | }; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tsconfig/strictest/tsconfig", 4 | "@tsconfig/node18/tsconfig", 5 | "@tsconfig/next/tsconfig" 6 | ], 7 | "compilerOptions": { 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "stripInternal": true, 12 | "allowJs": true, 13 | "noEmit": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "noImplicitReturns": false, 20 | "noPropertyAccessFromIndexSignature": false, 21 | "exactOptionalPropertyTypes": false, 22 | "moduleResolution": "node" 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------