init
105
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Privy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
39
README.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Privy Auth `create-next-app` Starter
|
||||
|
||||
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/)!
|
||||
|
||||
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).
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone this repository and open it in your terminal.
|
||||
```sh
|
||||
git clone https://github.com/privy-io/create-next-app
|
||||
```
|
||||
|
||||
2. Install the necessary dependencies (including [Privy Auth](https://www.npmjs.com/package/@privy-io/react-auth)) with `npm`.
|
||||
```sh
|
||||
npm i
|
||||
```
|
||||
|
||||
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).
|
||||
```sh
|
||||
# In your terminal, create .env.local from .env.example
|
||||
cp .env.example .env.local
|
||||
|
||||
# Add your Privy App ID to .env.local
|
||||
NEXT_PUBLIC_PRIVY_APP_ID=<your-privy-app-id>
|
||||
```
|
||||
|
||||
## Building locally
|
||||
|
||||
In your project directory, run `npm run dev`. You can now visit http://localhost:3000 to see your app and login with Privy!
|
||||
|
||||
|
||||
## Check out:
|
||||
- `pages/_app.tsx` for how to use the `PrivyProvider` and initialize it with your Privy App ID
|
||||
- `pages/index.tsx` for how to use the `usePrivy` hook and implement a simple `login` button
|
||||
- `pages/dashboard.tsx` for how to use the `usePrivy` hook, fields like `ready`, `authenticated`, and `user`, and methods like `linkWallet` and `logout`
|
||||
|
||||
|
||||
**Check out [our docs](https://docs.privy.io/) for more guidance around using Privy in your app!**
|
||||
0
components/.keep
Normal file
14
components/formatted-date.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
type Props = {
|
||||
secsSinceEpoch: number;
|
||||
};
|
||||
|
||||
export default function FormattedDate({secsSinceEpoch}: Props) {
|
||||
const formattedDate = new Date(secsSinceEpoch * 1000).toLocaleDateString('en-us', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
return <p>{formattedDate}</p>;
|
||||
}
|
||||
913
components/graphics/login.tsx
Normal file
1904
components/graphics/portal.tsx
Normal file
30
components/layout.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import {usePrivy} from '@privy-io/react-auth';
|
||||
import Navbar from './navbar';
|
||||
import type {NavbarItem} from './navbar';
|
||||
import {useRouter} from 'next/router';
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
accountId: string;
|
||||
appName: string;
|
||||
navbarItems: Array<NavbarItem>;
|
||||
};
|
||||
|
||||
export default function Layout({children, accountId, appName, navbarItems}: Props) {
|
||||
const {ready, authenticated} = usePrivy();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (ready && !authenticated) {
|
||||
router.push('/');
|
||||
}
|
||||
}, [ready, authenticated, router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar accountId={accountId} appName={appName} items={navbarItems} />
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
components/logo.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
interface LogoPropsType {
|
||||
fontColor?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export function Logo(props: LogoPropsType) {
|
||||
const fontColor = props.fontColor || 'white';
|
||||
const width = props.width || '151';
|
||||
const height = props.height || '44';
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_1922_14635)">
|
||||
<path
|
||||
d="M30.263 37.5605C30.263 39.2539 23.8314 40.6266 15.8976 40.6266C7.96384 40.6266 1.53223 39.2539 1.53223 37.5605C1.53223 35.8671 7.96384 34.4943 15.8976 34.4943C23.8314 34.4943 30.263 35.8671 30.263 37.5605Z"
|
||||
fill="#FF8271"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.455615 24.2448C0.455615 29.1946 5.01917 31.4142 9.48116 31.428C19.9212 31.428 31.4722 25.0107 31.4121 14.2713C31.3688 6.52859 24.2544 -0.0628907 15.8357 0.000452793C7.82477 0.000452793 0.000529064 5.23097 0 12.3567C0 14.2703 1.09781 16.1216 3.96753 16.33C1.57478 18.6994 0.455615 21.5383 0.455615 24.2448ZM15.8632 17.2471C17.5367 17.2471 18.8934 15.617 18.8934 13.6061C18.8934 11.5952 17.5367 9.96502 15.8632 9.96502C14.1897 9.96502 12.8331 11.5952 12.8331 13.6061C12.8331 15.617 14.1897 17.2471 15.8632 17.2471ZM24.5514 17.2472C26.2249 17.2472 27.5816 15.617 27.5816 13.6061C27.5816 11.5952 26.2249 9.96503 24.5514 9.96503C22.8779 9.96503 21.5213 11.5952 21.5213 13.6061C21.5213 15.617 22.8779 17.2472 24.5514 17.2472Z"
|
||||
fill="#FF8271"
|
||||
/>
|
||||
<path
|
||||
d="M45.3549 33.1573C45.864 33.1573 46.2806 32.7376 46.2806 32.2246C46.2806 29.5186 46.2806 26.8126 46.2806 24.1066C47.9007 24.1066 49.5208 24.1066 51.1408 24.1066C56.7416 24.1066 60.8612 19.9557 60.8612 14.3125C60.8612 8.66924 56.7416 4.51841 51.1408 4.51841H42.8091C42.2999 4.51841 41.8833 4.93816 41.8833 5.45118V32.2246C41.8833 32.7376 42.2999 33.1573 42.8091 33.1573H45.3549ZM46.2806 19.6759C46.2806 16.1003 46.2806 12.5247 46.2806 8.94907C47.9007 8.94907 49.5208 8.94907 51.1408 8.94907C54.1958 8.94907 56.325 11.1411 56.325 14.3125C56.325 17.4839 54.1958 19.6759 51.1408 19.6759C49.5208 19.6759 47.9007 19.6759 46.2806 19.6759Z"
|
||||
fill={fontColor}
|
||||
/>
|
||||
<path
|
||||
d="M66.8607 33.1573C67.3699 33.1573 67.7865 32.7376 67.7865 32.2246V21.0313C67.7865 16.0877 71.0729 14.2221 74.7296 14.2221H75.6553C76.1645 14.2221 76.5811 13.8024 76.5811 13.2894V10.7709C76.5811 10.2579 76.1645 9.83811 75.6553 9.83811H74.7296C69.1741 9.83811 67.2553 13.6935 67.2553 13.6935C67.2553 13.6935 67.2553 11.2893 67.2553 10.7709C67.2553 10.0349 66.8387 9.83811 66.3295 9.83811H64.5463C64.0372 9.83811 63.6206 10.2579 63.6206 10.7709V32.2246C63.6206 32.7376 64.0372 33.1573 64.5463 33.1573H66.8607Z"
|
||||
fill={fontColor}
|
||||
/>
|
||||
<path
|
||||
d="M82.5485 33.1573C83.0576 33.1573 83.4742 32.7376 83.4742 32.2246V10.7709C83.4742 10.2579 83.0576 9.83811 82.5485 9.83811H80.1415C79.6324 9.83811 79.2158 10.2579 79.2158 10.7709V32.2246C79.2158 32.7376 79.6324 33.1573 80.1415 33.1573H82.5485Z"
|
||||
fill={fontColor}
|
||||
/>
|
||||
<path
|
||||
d="M97.872 33.1573C98.2885 33.1573 98.5663 32.9241 98.7514 32.4578L106.759 11.2839C106.852 11.0507 106.944 10.7709 106.944 10.491C106.944 10.1646 106.574 9.83811 106.019 9.83811H103.149C102.732 9.83811 102.455 10.0247 102.269 10.5377C100.396 15.9569 96.57 26.4462 96.57 26.4462C96.57 26.4462 92.7605 15.9694 90.8826 10.5377C90.6974 10.0247 90.4197 9.83811 90.0031 9.83811H87.1333C86.5778 9.83811 86.2075 10.1646 86.2075 10.5377C86.2075 10.7709 86.3001 11.0507 86.3927 11.2839L94.4467 32.4578C94.6318 32.9241 94.9096 33.1573 95.3262 33.1573H97.872Z"
|
||||
fill={fontColor}
|
||||
/>
|
||||
<path
|
||||
d="M117.084 40.6266C117.5 40.6266 117.778 40.4401 117.963 39.9271L128.815 11.2839C128.907 11.0507 129 10.7709 129 10.5377C129 10.1646 128.63 9.83811 128.074 9.83811H125.251C124.834 9.83811 124.556 10.0247 124.371 10.5377C122.635 15.5261 118.909 26.4462 118.909 26.4462C118.909 26.4462 114.924 15.5774 113.031 10.5377C112.846 10.0247 112.568 9.83811 112.151 9.83811H109.328C108.772 9.83811 108.402 10.1646 108.402 10.5377C108.402 10.7709 108.495 11.0507 108.587 11.2839L116.317 31.0586C116.41 31.2918 116.456 31.4317 116.456 31.525C116.456 31.6649 116.41 31.7582 116.317 32.038L113.797 39.2741C113.705 39.5073 113.658 39.6939 113.658 39.8804C113.658 40.2535 113.982 40.6266 114.584 40.6266H117.084Z"
|
||||
fill={fontColor}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1922_14635">
|
||||
<rect width={width} height={height} fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
192
components/navbar.tsx
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import Image from 'next/image';
|
||||
import {useRouter} from 'next/router';
|
||||
import {Fragment} from 'react';
|
||||
import {Disclosure, Menu, Transition} from '@headlessui/react';
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
Bars3Icon,
|
||||
XMarkIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import {Logo} from './logo';
|
||||
|
||||
function classNames(...classes: Array<string | boolean>): string {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* make sure you are passing router.pathname and not
|
||||
* router.asPath since we want to have stripped any
|
||||
* fragments, query params, or trailing slashes
|
||||
*/
|
||||
const extractTabFromPath = (path: string) => {
|
||||
return path.split('/').pop() as string;
|
||||
};
|
||||
|
||||
export type NavbarItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
resource: string;
|
||||
};
|
||||
|
||||
type NavbarProps = {
|
||||
accountId: string;
|
||||
appName: string;
|
||||
items: Array<NavbarItem>;
|
||||
};
|
||||
|
||||
export default function Navbar({items, accountId, appName}: NavbarProps) {
|
||||
const router = useRouter();
|
||||
const resourceId = router.query.id;
|
||||
const selected = extractTabFromPath(router.pathname);
|
||||
|
||||
const selectedItemClass =
|
||||
'hover:cursor-pointer rounded-full bg-gray-900 px-3 py-2 text-lg font-medium text-white';
|
||||
const unselectedItemClass =
|
||||
'hover:cursor-pointer rounded-full px-3 py-2 text-lg font-medium text-gray-300 hover:bg-gray-700 hover:text-white';
|
||||
|
||||
// Navigate to a resource sub-page:
|
||||
// /apps/:appId/settings
|
||||
// /accounts/:accountId/users
|
||||
const navigateTo = (item: NavbarItem) => {
|
||||
router.push(`/${item.resource}/${resourceId}/${item.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure as="nav" className="bg-gray-800">
|
||||
{({open}) => (
|
||||
<>
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="block h-8 w-auto lg:hidden mb-2">
|
||||
<Logo />
|
||||
</div>
|
||||
<div className="hidden h-8 w-auto lg:block mb-2 hover:cursor-pointer">
|
||||
<Logo />
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:block">
|
||||
<div className="flex space-x-4">
|
||||
{items ? (
|
||||
items.map((item) => {
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
navigateTo(item);
|
||||
}}
|
||||
className={
|
||||
selected === item.id ? selectedItemClass : unselectedItemClass
|
||||
}
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:block">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white"
|
||||
>
|
||||
<InformationCircleIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
<p className="text-white">{appName}</p>
|
||||
|
||||
{/* Profile dropdown */}
|
||||
<Menu as="div" className="relative ml-3">
|
||||
<div className="flex bg-gray-800 rounded-full items-center hover:ring-white hover:ring-2 hover:ring-offset-2 hover:ring-offset-gray-800 hover:outline-none hover:cursor-pointer">
|
||||
<Menu.Button className="flex rounded-full text-sm">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<div className="h-8 w-8 rounded-full">
|
||||
<Image
|
||||
className="h-8 w-8 rounded-full"
|
||||
src="/images/avatar.png"
|
||||
alt="avatar placeholder"
|
||||
height="32px"
|
||||
width="32px"
|
||||
/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
<ChevronDownIcon className="ml-1 h-4 w-4 text-white" aria-hidden="true" />
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
<a
|
||||
href={`/accounts/${accountId}`}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700',
|
||||
)}
|
||||
>
|
||||
Your account
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
<a
|
||||
href="#"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700',
|
||||
)}
|
||||
>
|
||||
Settings
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
<a
|
||||
href="#"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700',
|
||||
)}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mr-2 flex sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<Bars3Icon className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
10
extensions.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"tamasfe.even-better-toml",
|
||||
"mikestead.dotenv",
|
||||
"austenc.tailwind-docs",
|
||||
"styled-components.vscode-styled-components"
|
||||
]
|
||||
}
|
||||
5
next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
4
next.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
15218
package-lock.json
generated
Normal file
39
package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"private": true,
|
||||
"engines": {
|
||||
"npm": ">=9.0.0",
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"format": "npx prettier --write \"{__tests__,components,pages,styles}/**/*.{ts,tsx,js,jsx}\"",
|
||||
"lint": "next lint && npx prettier --check \"{__tests__,components,pages,styles}/**/*.{ts,tsx,js,jsx}\" && npx tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@privy-io/react-auth": "1.76.4",
|
||||
"@privy-io/server-auth": "1.9.5",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"next": "latest",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/next": "^2.0.0",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
"@tsconfig/strictest": "^2.0.1",
|
||||
"@types/node": "^18",
|
||||
"@types/react": "18.2.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-next": "12.2.5",
|
||||
"postcss": "^8.4.14",
|
||||
"tailwindcss": "^3.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
||||
57
pages/_app.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import "../styles/globals.css";
|
||||
import type { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import { PrivyProvider } from "@privy-io/react-auth";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/AdelleSans-Regular.woff"
|
||||
as="font"
|
||||
crossOrigin=""
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/AdelleSans-Regular.woff2"
|
||||
as="font"
|
||||
crossOrigin=""
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/AdelleSans-Semibold.woff"
|
||||
as="font"
|
||||
crossOrigin=""
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/AdelleSans-Semibold.woff2"
|
||||
as="font"
|
||||
crossOrigin=""
|
||||
/>
|
||||
|
||||
<link rel="icon" href="/favicons/favicon.ico" sizes="any" />
|
||||
<link rel="icon" href="/favicons/icon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/favicons/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/favicons/manifest.json" />
|
||||
|
||||
<title>Privy Auth Starter</title>
|
||||
<meta name="description" content="Privy Auth Starter" />
|
||||
</Head>
|
||||
<PrivyProvider
|
||||
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ""}
|
||||
config={{
|
||||
embeddedWallets: {
|
||||
createOnLogin: "all-users",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</PrivyProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
37
pages/api/verify.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { PrivyClient, AuthTokenClaims } from "@privy-io/server-auth";
|
||||
|
||||
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
|
||||
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
|
||||
const client = new PrivyClient(PRIVY_APP_ID!, PRIVY_APP_SECRET!);
|
||||
|
||||
export type AuthenticateSuccessResponse = {
|
||||
claims: AuthTokenClaims;
|
||||
};
|
||||
|
||||
export type AuthenticationErrorResponse = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<
|
||||
AuthenticateSuccessResponse | AuthenticationErrorResponse
|
||||
>,
|
||||
) {
|
||||
const headerAuthToken = req.headers.authorization?.replace(/^Bearer /, "");
|
||||
const cookieAuthToken = req.cookies["privy-token"];
|
||||
|
||||
const authToken = cookieAuthToken || headerAuthToken;
|
||||
if (!authToken) return res.status(401).json({ error: "Missing auth token" });
|
||||
|
||||
try {
|
||||
const claims = await client.verifyAuthToken(authToken);
|
||||
return res.status(200).json({ claims });
|
||||
} catch (e: any) {
|
||||
return res.status(401).json({ error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
export default handler;
|
||||
146
pages/dashboard.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getAccessToken, usePrivy } from "@privy-io/react-auth";
|
||||
import Head from "next/head";
|
||||
|
||||
async function verifyToken() {
|
||||
const url = "/api/verify";
|
||||
const accessToken = await getAccessToken();
|
||||
const result = await fetch(url, {
|
||||
headers: {
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined),
|
||||
},
|
||||
});
|
||||
|
||||
return await result.json();
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [verifyResult, setVerifyResult] = useState();
|
||||
const router = useRouter();
|
||||
const {
|
||||
ready,
|
||||
authenticated,
|
||||
user,
|
||||
logout,
|
||||
linkEmail,
|
||||
linkWallet,
|
||||
unlinkEmail,
|
||||
linkPhone,
|
||||
unlinkPhone,
|
||||
unlinkWallet,
|
||||
linkGoogle,
|
||||
unlinkGoogle,
|
||||
linkTwitter,
|
||||
unlinkTwitter,
|
||||
linkDiscord,
|
||||
unlinkDiscord,
|
||||
} = usePrivy();
|
||||
|
||||
useEffect(() => {
|
||||
if (ready && !authenticated) {
|
||||
router.push("/");
|
||||
}
|
||||
}, [ready, authenticated, router]);
|
||||
|
||||
const numAccounts = user?.linkedAccounts?.length || 0;
|
||||
const canRemoveAccount = numAccounts > 1;
|
||||
|
||||
const email = user?.email;
|
||||
const phone = user?.phone;
|
||||
const wallet = user?.wallet;
|
||||
|
||||
const googleSubject = user?.google?.subject || null;
|
||||
const twitterSubject = user?.twitter?.subject || null;
|
||||
const discordSubject = user?.discord?.subject || null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Account Settings</title>
|
||||
</Head>
|
||||
|
||||
<main className="flex flex-col min-h-screen px-4 sm:px-20 py-6 sm:py-10 bg-black text-white">
|
||||
{ready && authenticated ? (
|
||||
<>
|
||||
<div className="flex flex-row justify-between">
|
||||
<h1 className="text-2xl font-semibold text-white">Welcome Warlock,</h1>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="text-sm bg-red-500 hover:bg-red-700 py-2 px-4 rounded-md text-violet-100"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
<div className="items-center flex flex-wrap justify-center p-20">
|
||||
<h1 className="text-4xl">$0.1</h1>
|
||||
</div>
|
||||
<div className="mt-12 flex gap-4 flex-wrap">
|
||||
|
||||
{twitterSubject ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
unlinkTwitter(twitterSubject);
|
||||
}}
|
||||
className="text-sm border border-violet-600 hover:border-violet-700 py-2 px-4 rounded-md text-violet-600 hover:text-violet-700 disabled:border-gray-500 disabled:text-gray-500 hover:disabled:text-gray-500"
|
||||
disabled={!canRemoveAccount}
|
||||
>
|
||||
Unlink Twitter
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="text-sm bg-violet-600 hover:bg-violet-700 py-2 px-4 rounded-md text-white"
|
||||
onClick={() => {
|
||||
linkTwitter();
|
||||
}}
|
||||
>
|
||||
Link Twitter
|
||||
</button>
|
||||
)}
|
||||
|
||||
{discordSubject ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
unlinkDiscord(discordSubject);
|
||||
}}
|
||||
className="text-sm border border-violet-600 hover:border-violet-700 py-2 px-4 rounded-md text-violet-600 hover:text-violet-700 disabled:border-gray-500 disabled:text-gray-500 hover:disabled:text-gray-500"
|
||||
disabled={!canRemoveAccount}
|
||||
>
|
||||
Unlink Discord
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="text-sm bg-violet-600 hover:bg-violet-700 py-2 px-4 rounded-md text-white"
|
||||
onClick={() => {
|
||||
linkDiscord();
|
||||
}}
|
||||
>
|
||||
Link Discord
|
||||
</button>
|
||||
)}
|
||||
{wallet ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
unlinkWallet(wallet.address);
|
||||
}}
|
||||
className="text-sm border border-violet-600 hover:border-violet-700 py-2 px-4 rounded-md text-violet-600 hover:text-violet-700 disabled:border-gray-500 disabled:text-gray-500 hover:disabled:text-gray-500"
|
||||
disabled={!canRemoveAccount}
|
||||
>
|
||||
Unlink wallet
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={linkWallet}
|
||||
className="text-sm bg-violet-600 hover:bg-violet-700 py-2 px-4 rounded-md text-white border-none"
|
||||
>
|
||||
Connect wallet
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</>
|
||||
) : null}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
64
pages/index.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import Portal from "../components/graphics/portal";
|
||||
import { useLogin } from "@privy-io/react-auth";
|
||||
import { PrivyClient } from "@privy-io/server-auth";
|
||||
import { GetServerSideProps } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
|
||||
const cookieAuthToken = req.cookies["privy-token"];
|
||||
|
||||
// If no cookie is found, skip any further checks
|
||||
if (!cookieAuthToken) return { props: {} };
|
||||
|
||||
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
|
||||
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
|
||||
const client = new PrivyClient(PRIVY_APP_ID!, PRIVY_APP_SECRET!);
|
||||
|
||||
try {
|
||||
const claims = await client.verifyAuthToken(cookieAuthToken);
|
||||
// Use this result to pass props to a page for server rendering or to drive redirects!
|
||||
// ref https://nextjs.org/docs/pages/api-reference/functions/get-server-side-props
|
||||
console.log({ claims });
|
||||
|
||||
return {
|
||||
props: {},
|
||||
redirect: { destination: "/dashboard", permanent: false },
|
||||
};
|
||||
} catch (error) {
|
||||
return { props: {} };
|
||||
}
|
||||
};
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { login } = useLogin({
|
||||
onComplete: () => router.push("/dashboard"),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Login · Privy</title>
|
||||
</Head>
|
||||
|
||||
<main className="flex min-h-screen min-w-full">
|
||||
<div className="flex bg-black flex-1 p-6 justify-center items-center">
|
||||
<div>
|
||||
<div>
|
||||
{/* <Portal style={{ maxWidth: "100%", height: "auto" }} /> */}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-center text-center">
|
||||
<button
|
||||
className="bg-violet-600 hover:bg-violet-700 py-3 px-6 text-white rounded-lg"
|
||||
onClick={login}
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
public/favicons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/favicons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/favicons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/favicons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
3
public/favicons/icon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="59" height="59" viewBox="0 0 59 59" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.855755 45.3546C0.855755 54.6141 9.42722 58.7663 17.8079 58.7922C37.4169 58.7922 59.1124 46.7874 58.9996 26.6972C58.9182 12.213 45.5557 -0.117649 29.7433 0.000847037C14.6968 0.000847037 0.000993712 9.78554 0 23.1156C0 26.6954 2.06195 30.1585 7.45199 30.5484C2.95783 34.9808 0.855755 40.2915 0.855755 45.3546ZM29.795 32.2641C32.9383 32.2641 35.4864 29.2146 35.4864 25.4528C35.4864 21.691 32.9383 18.6415 29.795 18.6415C26.6518 18.6415 24.1037 21.691 24.1037 25.4528C24.1037 29.2146 26.6518 32.2641 29.795 32.2641ZM46.1135 32.2642C49.2568 32.2642 51.8049 29.2146 51.8049 25.4528C51.8049 21.691 49.2568 18.6415 46.1135 18.6415C42.9703 18.6415 40.4222 21.691 40.4222 25.4528C40.4222 29.2146 42.9703 32.2642 46.1135 32.2642Z" fill="#FF8271"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 892 B |
7
public/favicons/manifest.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Privy Auth Demo",
|
||||
"icons": [
|
||||
{"src": "/favicons/android-chrome-192x192.png", "type": "image/png", "sizes": "192x192"},
|
||||
{"src": "/favicons/android-chrome-512x512.png", "type": "image/png", "sizes": "512x512"}
|
||||
]
|
||||
}
|
||||
BIN
public/fonts/AdelleSans-Regular.woff
Normal file
BIN
public/fonts/AdelleSans-Regular.woff2
Normal file
BIN
public/fonts/AdelleSans-Semibold.woff
Normal file
BIN
public/fonts/AdelleSans-Semibold.woff2
Normal file
BIN
public/images/avatar.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
public/logos/privy-logo.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/logos/privy-logomark.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
22
renovate.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"encrypted": {
|
||||
"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"
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"packagePatterns": ["*"],
|
||||
"excludePackagePatterns": ["@privy-io"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"rangeStrategy": "pin",
|
||||
"constraints": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
23
styles/globals.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
/* normal */
|
||||
@font-face {
|
||||
font-family: "Adelle Sans";
|
||||
src: url("/fonts/AdelleSans-Regular.woff2") format("woff2");
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* semibold */
|
||||
@font-face {
|
||||
font-family: "Adelle Sans";
|
||||
src: url("/fonts/AdelleSans-Semibold.woff2") format("woff2");
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
20
tailwind.config.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Adelle Sans', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
colors: {
|
||||
'privy-navy': '#160B45',
|
||||
'privy-light-blue': '#EFF1FD',
|
||||
'privy-blueish': '#D4D9FC',
|
||||
'privy-pink': '#FF8271',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
};
|
||||
26
tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"extends": [
|
||||
"@tsconfig/strictest/tsconfig",
|
||||
"@tsconfig/node18/tsconfig",
|
||||
"@tsconfig/next/tsconfig"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"stripInternal": true,
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"module": "esnext",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"noImplicitReturns": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||