├── .gitignore ├── LICENSE ├── README.md ├── docs ├── acme.png ├── oasis-instance-settings.png ├── oasis-url-and-redirects-settings.png ├── oasis.png ├── simple.png ├── widget-integrations-webhooks.png ├── widget-url-and-redirects-settings.png ├── widget-user-created-webhook.png └── widget.png └── examples ├── acme ├── .env ├── .eslintrc.json ├── .gitignore ├── README.md ├── assets │ └── svg │ │ └── logo.svg ├── components │ ├── GithubLink.module.css │ ├── GithubLink.tsx │ ├── Home.module.css │ ├── Home.tsx │ ├── NavBar.module.css │ └── NavBar.tsx ├── layouts │ ├── AuthLayout.tsx │ ├── HomeLayout.module.css │ └── HomeLayout.tsx ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── index.tsx │ ├── my-profile │ │ ├── [[...index]].tsx │ │ ├── custom-page.tsx │ │ └── security.tsx │ ├── sign-in │ │ └── [[...index]].tsx │ └── sign-up │ │ └── [[...index]].tsx ├── public │ ├── arrow-right.svg │ ├── clerk.svg │ ├── favicon.png │ └── images │ │ └── bg.png ├── styles │ ├── App.css │ └── globals.css └── tsconfig.json ├── oasis ├── .eslintrc.json ├── .gitignore ├── README.md ├── assets │ └── svg │ │ ├── logo-old.svg │ │ ├── logo.svg │ │ ├── naked-logo-old.svg │ │ ├── naked-logo.svg │ │ └── upload.svg ├── components │ ├── Button.module.css │ ├── Button.tsx │ ├── CustomLink.module.css │ ├── CustomLink.tsx │ ├── Dashboard.module.css │ ├── Dashboard.tsx │ ├── GithubLink.module.css │ ├── GithubLink.tsx │ ├── Home.module.css │ ├── Home.tsx │ ├── Input.module.css │ ├── Input.tsx │ ├── SignIn.tsx │ ├── Title.module.css │ ├── Title.tsx │ ├── layout │ │ ├── Card.module.css │ │ ├── Card.tsx │ │ ├── FormLayout.module.css │ │ ├── FormLayout.tsx │ │ ├── MainLayout.module.css │ │ └── MainLayout.tsx │ ├── profile │ │ ├── Profile.module.css │ │ ├── Profile.tsx │ │ ├── ProfileCompleteStep.tsx │ │ ├── ProfileUploadPhotoStep.tsx │ │ └── index.ts │ └── signUp │ │ ├── SignUp.module.css │ │ ├── SignUp.tsx │ │ ├── SignUpCodeStep.tsx │ │ ├── SignUpEmailStep.tsx │ │ ├── SignUpFirstNameStep.tsx │ │ ├── SignUpLastNameStep.tsx │ │ ├── SignUpUsernameStep.tsx │ │ └── index.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── create-profile.tsx │ ├── dashboard.tsx │ ├── index.tsx │ ├── sign-in │ │ └── [[...index]].tsx │ └── sign-up │ │ └── [[...index]].tsx ├── public │ ├── arrow-right.svg │ ├── clerk.svg │ └── favicon.png ├── styles │ └── index.css ├── svg │ ├── logo.svg │ ├── naked-logo.svg │ └── upload.svg ├── tsconfig.json └── utils │ └── errors.ts ├── simple ├── .eslintrc.json ├── .gitignore ├── README.md ├── components │ ├── Header.module.css │ ├── Header.tsx │ └── clerk │ │ ├── GithubLink.module.css │ │ └── GithubLink.tsx ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── hello-js.js │ │ └── hello.ts │ ├── index.tsx │ └── user │ │ └── [[...index]].tsx ├── public │ ├── arrow-right.svg │ ├── clerk.svg │ └── favicon.png ├── styles │ ├── Home.module.css │ └── globals.css └── tsconfig.json └── widget ├── .env ├── .eslintrc.json ├── .gitignore ├── README.md ├── assets └── svg │ ├── ArrowLeft.svg │ ├── Checkmark.svg │ ├── Exclamation.svg │ ├── Logo.svg │ ├── Mark.svg │ └── USA.svg ├── components ├── Dashboard.module.css ├── Dashboard.tsx ├── GithubLink.module.css ├── GithubLink.tsx ├── Promotion.module.css ├── Promotion.tsx ├── SignInForm │ ├── SignInCode.module.css │ ├── SignInCode.tsx │ ├── SignInForm.module.css │ ├── SignInForm.tsx │ ├── SignInPassword.module.css │ ├── SignInPassword.tsx │ ├── VerificationSwitcher.module.css │ ├── VerificationSwitcher.tsx │ └── index.ts ├── SignUpForm │ ├── SignUpCode.module.css │ ├── SignUpCode.tsx │ ├── SignUpForm.module.css │ ├── SignUpForm.tsx │ ├── Terms.module.css │ ├── Terms.tsx │ └── index.tsx ├── common │ ├── Button.module.css │ ├── Button.tsx │ ├── ErrorMessage.module.css │ ├── ErrorMessage.tsx │ ├── Input.module.css │ ├── Input.tsx │ ├── Notice.module.css │ ├── Notice.tsx │ ├── Spinner.module.css │ ├── Spinner.tsx │ ├── Title.module.css │ ├── Title.tsx │ ├── VerifyCodeNotice.module.css │ └── VerifyCodeNotice.tsx └── utils │ ├── errors.ts │ └── formValidations.ts ├── db ├── README.md └── prisma │ ├── index.ts │ ├── migrations │ ├── 20211103081338_init │ │ └── migration.sql │ └── migration_lock.toml │ └── schema.prisma ├── layouts ├── SignInLayout.module.css ├── SignInLayout.tsx ├── SignUpLayout.module.css └── SignUpLayout.tsx ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ └── webhooks │ │ ├── README.md │ │ └── user.ts ├── dashboard.tsx ├── sign-in │ └── [[...index]].tsx └── sign-up │ └── [[...index]].tsx ├── public ├── arrow-right.svg ├── clerk.svg └── favicon.png ├── server ├── .gitignore └── userRepo.ts ├── styles ├── globals.css └── variables.css └── tsconfig.json /.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 | .yarn 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Clerk 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 |

2 | 3 | Clerk logo 4 | 5 |
6 |

7 | 8 | # Clerk Next.js Examples 9 | 10 |
11 | 12 | [![Chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) 13 | [![Clerk documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.dev/docs?utm_source=github&utm_medium=clerk_nextjs_examples) 14 | [![Follow on Twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) 15 | 16 |
17 | 18 | --- 19 | 20 | ### Clerk is Hiring! 21 | 22 | Would you like to work on Open Source software and help maintain this repository? Apply today https://apply.workable.com/clerk-dev/. 23 | 24 | --- 25 | 26 | This repository holds sample code for different types of authentication flows you might need for your Next.js application. All built using [Clerk](https://clerk.dev?utm_source=github&utm_medium=starters&utm_campaign=nextjs-examples)! 27 | 28 | ### 1. Acme 29 | 30 | Custom sign up screen using the ClerkJS Components. 31 | 32 | [Live demo](https://nextjs.acme.clerk.app/) 33 | 34 | 35 | 36 | ### 2. Oasis 37 | 38 | Custom multi-step authentication form using Clerk hooks. 39 | 40 | [Live demo](https://nextjs.oasis.clerk.app/) 41 | 42 | 43 | 44 | 45 | ### 3. Simple 46 | 47 | The simplest setup with standard authentication capabilities. 48 | 49 | [Live demo](https://nextjs.simple.clerk.app/) 50 | 51 | 52 | 53 | ### 4. Widget 54 | 55 | Collect custom user data during sign up using Clerk hooks. 56 | 57 | [Live demo](https://nextjs.widget.clerk.app/) 58 | 59 | 60 | 61 | 62 | ## Sign up for Clerk 63 | 64 | To sign up, go to [https://clerk.dev](https://clerk.dev?utm_source=github&utm_medium=starters&utm_campaign=nextjs-examples), create your account and start building your user authentication! 65 | 66 | ## Having trouble ? 67 | 68 | If you find any bug, something is not working as expected or you would like to see if we can support your use case, you can reach out to any of our [support channels](https://clerk.dev/support?utm_source=github&utm_medium=starters&utm_campaign=nextjs-examples), or just open a new issue! 69 | -------------------------------------------------------------------------------- /docs/acme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/acme.png -------------------------------------------------------------------------------- /docs/oasis-instance-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/oasis-instance-settings.png -------------------------------------------------------------------------------- /docs/oasis-url-and-redirects-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/oasis-url-and-redirects-settings.png -------------------------------------------------------------------------------- /docs/oasis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/oasis.png -------------------------------------------------------------------------------- /docs/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/simple.png -------------------------------------------------------------------------------- /docs/widget-integrations-webhooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/widget-integrations-webhooks.png -------------------------------------------------------------------------------- /docs/widget-url-and-redirects-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/widget-url-and-redirects-settings.png -------------------------------------------------------------------------------- /docs/widget-user-created-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/widget-user-created-webhook.png -------------------------------------------------------------------------------- /docs/widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/docs/widget.png -------------------------------------------------------------------------------- /examples/acme/.env: -------------------------------------------------------------------------------- 1 | # Go to and create your application https://clerk.dev/docs/quickstarts/get-started-with-nextjs?utm_source=github&utm_medium=starters&utm_campaign=acme 2 | 3 | NEXT_PUBLIC_CLERK_FRONTEND_API= 4 | -------------------------------------------------------------------------------- /examples/acme/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/acme/.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 | -------------------------------------------------------------------------------- /examples/acme/README.md: -------------------------------------------------------------------------------- 1 | # Clerk Acme Starter 2 | 3 | ![Preview](../../docs/acme.png) 4 | 5 | This Next.js project demonstrates how to use [Clerk Components](https://clerk.dev/docs/component-reference/overview?utm_source=github&utm_medium=starters&utm_campaign=acme) to build a comprehensive, custom sign-up and sign-in experience with minimum effort. It demonstrates how to build a custom My profile page using the `` component. 6 | 7 | [![Open in VS Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/clerkinc/clerk-nextjs-examples) 8 | 9 | ## Live demo 10 | 11 | https://nextjs.acme.clerk.app/ 12 | 13 | ## Getting Started 14 | 15 | ### 1. Setup environment variables 16 | 17 | First, you need to set two environment variables for Clerk's SDK. Go to the API Keys page and start by copying the Frontend API key. Then, add it a .env.local file in your application root, with the name `NEXT_PUBLIC_CLERK_FRONTEND_API`: 18 | 19 | ```bash 20 | # Replace [frontend-api-key] with your actual Frontend API key 21 | echo "NEXT_PUBLIC_CLERK_FRONTEND_API=[frontend-api-key]" >> .env.local 22 | ``` 23 | 24 | Next, go back to the API Keys page and copy the Backend API key. Then, add it in the same .env.local file, with the name `CLERK_API_KEY`: 25 | 26 | ```bash 27 | # Replace [backend-api-key] with your actual Backend API key 28 | echo "CLERK_API_KEY=[backend-api-key]" >> .env.local 29 | 30 | ``` 31 | 32 | ### 2. Run the development server 33 | 34 | ```bash 35 | npm run dev 36 | ``` 37 | 38 | Lastly, open [http://localhost:3000](http://localhost:3000) with your browser. 39 | 40 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 41 | 42 | ## Learn More 43 | 44 | To learn more about Next.js, take a look at the following resources: 45 | 46 | - [Next.js authentication with Clerk](https://clerk.dev/docs/quickstarts/get-started-with-nextjs) - Add secure, beautiful, and fast authentication to Next.js with Clerk. 47 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 48 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 49 | 50 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 51 | 52 | ## Deploy on Vercel 53 | 54 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 55 | 56 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 57 | -------------------------------------------------------------------------------- /examples/acme/assets/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/acme/components/GithubLink.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: "Inter", sans-serif; 3 | font-size: 0.75rem; 4 | font-weight: 400; 5 | line-height: 1rem; 6 | letter-spacing: 0em; 7 | text-align: left; 8 | 9 | display: flex; 10 | 11 | box-shadow: 0px 24px 48px 0px #00000029; 12 | border-radius: 6px; 13 | 14 | height: 40px; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 0 1.5rem; 18 | background-color: white; 19 | } 20 | 21 | .logo { 22 | position: relative; 23 | top: 2px; 24 | min-width: 56px; 25 | } 26 | 27 | .label { 28 | font-family: "Inter", sans-serif; 29 | font-size: 0.75rem; 30 | font-weight: 400; 31 | line-height: 1rem; 32 | letter-spacing: 0em; 33 | text-align: left; 34 | margin: 0 1.5rem; 35 | } 36 | 37 | .rightLink { 38 | color: #335bf1; 39 | min-width: 100px; 40 | } 41 | 42 | .rightLink a:link { 43 | text-decoration: none; 44 | color: #335bf1; 45 | } 46 | 47 | .rightLink a:visited { 48 | text-decoration: none; 49 | color: #335bf1; 50 | } 51 | 52 | .rightLink a:hover { 53 | text-decoration: underline; 54 | color: #335bf1; 55 | } 56 | 57 | .rightLink a:active { 58 | text-decoration: underline; 59 | color: #335bf1; 60 | } 61 | 62 | .rightArrow { 63 | position: relative; 64 | top: 2px; 65 | left: 3px; 66 | } 67 | -------------------------------------------------------------------------------- /examples/acme/components/GithubLink.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./GithubLink.module.css"; 2 | import Image from "next/image"; 3 | 4 | type GithubLinkProps = { 5 | label: string; 6 | repoLink: string; 7 | }; 8 | const GithubLink = (props: GithubLinkProps) => ( 9 | <> 10 |
11 | 12 | clerk 13 | 14 |
{props.label}
15 |
16 | 17 | View on Github 18 | 19 | -> 20 | 21 | 22 |
23 |
24 | 25 | ); 26 | 27 | export default GithubLink; 28 | -------------------------------------------------------------------------------- /examples/acme/components/Home.module.css: -------------------------------------------------------------------------------- 1 | .heading { 2 | font-size: 24px; 3 | font-weight: 500; 4 | color: rgb(49 46 152); 5 | } 6 | 7 | .subHeading { 8 | font-size: 16px; 9 | font-weight: 400; 10 | color: rgb(49 46 152 / 70%); 11 | display: block; 12 | margin-bottom: 48px; 13 | } 14 | 15 | .grid { 16 | display: flex; 17 | gap: 32px; 18 | } 19 | 20 | .box { 21 | background: white; 22 | box-shadow: 0px 12px 24px rgba(0, 0, 0, 0.2); 23 | border-radius: 16px; 24 | height: 372px; 25 | width: 416px; 26 | } 27 | -------------------------------------------------------------------------------- /examples/acme/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Home.module.css"; 2 | 3 | export function Home() { 4 | return ( 5 | <> 6 | Home 7 | Welcome to your application 8 |
9 |
10 |
11 |
12 |
13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/acme/components/NavBar.module.css: -------------------------------------------------------------------------------- 1 | .navbar > ul { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | 6 | font-size: 16px; 7 | font-family: "PT Sans"; 8 | font-weight: 500; 9 | } 10 | 11 | .navbar > ul > li { 12 | display: inline-block; 13 | padding: 0 2em 0 0; 14 | } 15 | -------------------------------------------------------------------------------- /examples/acme/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./NavBar.module.css"; 2 | import React from "react"; 3 | import Link from "next/link"; 4 | 5 | export function Navbar() { 6 | return ( 7 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/acme/layouts/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | export function AuthLayout({ children }: { children: React.ReactNode }) { 2 | return ( 3 |
4 |
{children}
5 |
6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /examples/acme/layouts/HomeLayout.module.css: -------------------------------------------------------------------------------- 1 | .fullWidth { 2 | width: 100%; 3 | } 4 | 5 | .logoRow { 6 | padding: 28px 64px 36px; 7 | display: flex; 8 | justify-content: space-between; 9 | } 10 | 11 | .nav { 12 | border-bottom: 1px solid rgb(49 46 152 / 30%); 13 | padding: 0 36px; 14 | } 15 | 16 | .navButton { 17 | border: none; 18 | background: none; 19 | color: rgb(49 46 152); 20 | padding: 16px 0px; 21 | margin: 0px 32px; 22 | cursor: pointer; 23 | border-bottom: 1px solid rgb(49 46 152); 24 | font-size: 16px; 25 | font-family: "PT Sans"; 26 | font-weight: 500; 27 | } 28 | 29 | .main { 30 | padding: 46px 64px; 31 | } 32 | -------------------------------------------------------------------------------- /examples/acme/layouts/HomeLayout.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./HomeLayout.module.css"; 2 | import Logo from "../assets/svg/logo.svg"; 3 | import { UserButton } from "@clerk/nextjs"; 4 | import React from "react"; 5 | 6 | export function HomeLayout({ children }: { children: React.ReactNode }) { 7 | return ( 8 |
9 |
10 |
11 | 12 | 13 |
14 | 17 |
18 |
{children}
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/acme/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/acme/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | webpack(config) { 5 | config.module.rules.push({ 6 | test: /\.svg$/, 7 | use: ["@svgr/webpack"], 8 | }); 9 | return config; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/acme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clerk-acme-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^3.7.0", 13 | "next": "12.2.2", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@svgr/webpack": "^5.5.0", 19 | "@types/react": "18.0.15", 20 | "eslint": "8.0.1", 21 | "eslint-config-next": "12.2.2", 22 | "typescript": "4.7.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/acme/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import "../styles/App.css"; 3 | import type { AppProps } from "next/app"; 4 | import { ClerkProvider, SignedIn, SignedOut } from "@clerk/nextjs"; 5 | import { useRouter } from "next/router"; 6 | import SignUpPage from "./sign-up/[[...index]]"; 7 | import SignInPage from "./sign-in/[[...index]]"; 8 | import GithubLink from "../components/GithubLink"; 9 | 10 | const theme = { 11 | general: { 12 | fontFamily: '"PT Sans"', 13 | }, 14 | }; 15 | 16 | function MyApp({ Component, pageProps }: AppProps) { 17 | const router = useRouter(); 18 | 19 | return ( 20 | 21 |
22 | 23 | 24 | 25 | 26 | {router.pathname.match("/sign-up") ? : } 27 | 28 |
29 |
30 | 34 |
35 |
36 | ); 37 | } 38 | export default MyApp; 39 | -------------------------------------------------------------------------------- /examples/acme/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default MyDocument; 24 | -------------------------------------------------------------------------------- /examples/acme/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { HomeLayout } from "../layouts/HomeLayout"; 3 | import { Home } from "../components/Home"; 4 | 5 | const Index: NextPage = () => { 6 | return ( 7 | 8 | ; 9 | 10 | ); 11 | }; 12 | 13 | export default Index; 14 | -------------------------------------------------------------------------------- /examples/acme/pages/my-profile/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { UserProfile } from "@clerk/nextjs"; 3 | import { HomeLayout } from "../../layouts/HomeLayout"; 4 | import { Navbar } from "../../components/NavBar"; 5 | 6 | export default function UserMyProfilePage() { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/acme/pages/my-profile/custom-page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HomeLayout } from "../../layouts/HomeLayout"; 3 | import { Navbar } from "../../components/NavBar"; 4 | 5 | export default function UserCustomPage1() { 6 | return ( 7 | 8 | 9 |
Custom page goes here
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/acme/pages/my-profile/security.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { UserProfile } from "@clerk/nextjs"; 3 | import { HomeLayout } from "../../layouts/HomeLayout"; 4 | import { Navbar } from "../../components/NavBar"; 5 | 6 | export default function UserSecurityPage() { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/acme/pages/sign-in/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | import { AuthLayout } from "../../layouts/AuthLayout"; 3 | 4 | const SignInPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default SignInPage; 13 | -------------------------------------------------------------------------------- /examples/acme/pages/sign-up/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | import { AuthLayout } from "../../layouts/AuthLayout"; 3 | 4 | const SignUpPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default SignUpPage; 13 | -------------------------------------------------------------------------------- /examples/acme/public/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/acme/public/clerk.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/acme/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/examples/acme/public/favicon.png -------------------------------------------------------------------------------- /examples/acme/public/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/examples/acme/public/images/bg.png -------------------------------------------------------------------------------- /examples/acme/styles/App.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Sidebar layout 3 | */ 4 | 5 | .container { 6 | height: 100vh; 7 | display: grid; 8 | grid-template-columns: minmax(480px, 25%) 1fr; 9 | text-align: center; 10 | } 11 | 12 | .cover { 13 | background: url("/images/bg.png") no-repeat center center; 14 | background-size: cover; 15 | } 16 | 17 | /** 18 | * Clerk Components CSS customization 19 | */ 20 | 21 | .cl-sign-in, .cl-sign-up { 22 | border: 0 !important; 23 | border-radius: 0 !important; 24 | box-shadow: none !important; 25 | } 26 | 27 | .cl-auth-form-body input { 28 | border: 0; 29 | border-bottom: 1px solid #e5e5e5; 30 | border-radius: 0; 31 | padding: 0; 32 | } 33 | 34 | .cl-auth-form-body input:focus { 35 | border-bottom: 1px solid #312e81; 36 | } 37 | 38 | .cl-auth-form-body label > span { 39 | color: rgba(0, 0, 0, 0.8); 40 | font-weight: normal; 41 | transition: transform 0.25s; 42 | transform: translateY(20px); 43 | } 44 | 45 | .cl-auth-form-body label:focus-within > span, 46 | .cl-auth-form-body input:not([value=""]) + span { 47 | transform: translateY(0px); 48 | } 49 | 50 | .cl-oauth-button-group { 51 | margin-bottom: 3em; 52 | } 53 | 54 | .cl-oauth-button-group::after { 55 | height: 0 !important; 56 | margin: 0 !important; 57 | background: none !important; 58 | } 59 | 60 | .cl-user-profile .cl-main { 61 | margin: 3em 0 0 0 !important; 62 | padding: 0 !important ; 63 | width: inherit; 64 | } 65 | -------------------------------------------------------------------------------- /examples/acme/styles/globals.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | margin: 0; 4 | font-family: "PT Sans", sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 11 | monospace; 12 | } 13 | 14 | footer { 15 | position: fixed; 16 | left: 0; 17 | right: 0; 18 | bottom: 0; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | margin: 20px; 23 | } 24 | -------------------------------------------------------------------------------- /examples/acme/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/oasis/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/oasis/.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 | -------------------------------------------------------------------------------- /examples/oasis/README.md: -------------------------------------------------------------------------------- 1 | # Clerk Oasis Starter 2 | 3 | ![Preview](../../docs/oasis.png) 4 | 5 | This Next.js project demonstrates how to use [@clerk/clerk-nextjs hooks](https://clerk.dev/docs/quickstarts/get-started-with-nextjs?utm_source=github&utm_medium=starters&utm_campaign=oasis) to build a custom sign-up and sign-in wizard with two-factor authentication and user profile attributes. 6 | 7 | The custom sign-in flow is implemented in a single component to demonstrate how to use Clerk with minimum effort. On the contrary, the custom sign-up flow has been fully componetized and leverages Clerk frontend state management. We also used [React-hook-form](https://react-hook-form.com) to build our forms. 8 | 9 | [![Open in VS Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/clerkinc/clerk-nextjs-examples) 10 | 11 | ## Live demo 12 | 13 | https://nextjs.oasis.clerk.app/ 14 | 15 | ## Getting Started 16 | 17 | ### Clerk Instance Setup 18 | 19 | The Oasis example requires a few Clerk instance settings to be set. 20 | 21 | 1. You have to enable passwordless authentication using OTP codes as described in the [official documentation](https://clerk.dev/docs/authentication/custom-flows/email-sms-otp). 22 | 23 | 2. You should request the name of your user under Personal information section. 24 | 25 | 3. You should require usernames. 26 | 27 | ### Development Setup 28 | 29 | ### 1. Setup environment variables 30 | 31 | First, you need to set two environment variables for Clerk's SDK. Go to the API Keys page and start by copying the Frontend API key. Then, add it a .env.local file in your application root, with the name `NEXT_PUBLIC_CLERK_FRONTEND_API`: 32 | 33 | ```bash 34 | # Replace [frontend-api-key] with your actual Frontend API key 35 | echo "NEXT_PUBLIC_CLERK_FRONTEND_API=[frontend-api-key]" >> .env.local 36 | ``` 37 | 38 | Next, go back to the API Keys page and copy the Backend API key. Then, add it in the same .env.local file, with the name `CLERK_API_KEY`: 39 | 40 | ```bash 41 | # Replace [backend-api-key] with your actual Backend API key 42 | echo "CLERK_API_KEY=[backend-api-key]" >> .env.local 43 | 44 | ``` 45 | 46 | ### 2. Run the development server 47 | 48 | ```bash 49 | npm run dev 50 | ``` 51 | 52 | Lastly, open [http://localhost:3000](http://localhost:3000) with your browser. 53 | 54 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 55 | 56 | ## Learn More 57 | 58 | To learn more about Next.js, take a look at the following resources: 59 | 60 | - [Next.js authentication with Clerk](https://clerk.dev/docs/quickstarts/get-started-with-nextjs) - Add secure, beautiful, and fast authentication to Next.js with Clerk. 61 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 62 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 63 | 64 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 65 | 66 | ## Deploy on Vercel 67 | 68 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 69 | 70 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 71 | -------------------------------------------------------------------------------- /examples/oasis/assets/svg/logo-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/oasis/assets/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/oasis/assets/svg/naked-logo-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/oasis/assets/svg/naked-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/oasis/assets/svg/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/oasis/components/Button.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background: #7431d7; 3 | font-size: 16px; 4 | line-height: 20px; 5 | color: #fff; 6 | font-family: "Nunito", sans-serif; 7 | font-style: normal; 8 | font-weight: 600; 9 | border: none; 10 | border-radius: 3px; 11 | min-width: 220px; 12 | padding: 16px 32px; 13 | cursor: pointer; 14 | } 15 | 16 | .button:disabled { 17 | opacity: 0.5; 18 | } 19 | -------------------------------------------------------------------------------- /examples/oasis/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Button.module.css"; 3 | 4 | const Button = React.forwardRef< 5 | HTMLButtonElement, 6 | { 7 | children: React.ReactNode; 8 | } & React.ButtonHTMLAttributes 9 | >(({ children, ...rest }, ref) => { 10 | return ( 11 | 14 | ); 15 | }); 16 | 17 | Button.displayName = "Button"; 18 | 19 | export { Button }; 20 | -------------------------------------------------------------------------------- /examples/oasis/components/CustomLink.module.css: -------------------------------------------------------------------------------- 1 | .link { 2 | text-decoration: underline; 3 | color: white; 4 | } 5 | -------------------------------------------------------------------------------- /examples/oasis/components/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import styles from "./CustomLink.module.css"; 4 | 5 | type CustomLinkProps = { 6 | href: string; 7 | } & React.AnchorHTMLAttributes; 8 | 9 | const CustomLink = ({ href, ...linkProps }: CustomLinkProps) => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | CustomLink.displayName = "CustomLink"; 18 | 19 | export { CustomLink }; 20 | -------------------------------------------------------------------------------- /examples/oasis/components/Dashboard.module.css: -------------------------------------------------------------------------------- 1 | .fullWidth { 2 | width: 100%; 3 | } 4 | 5 | .logoRow { 6 | padding: 28px 64px 36px; 7 | display: flex; 8 | justify-content: space-between; 9 | } 10 | 11 | .nav { 12 | border-bottom: 1px solid rgb(255 255 255 / 30%); 13 | padding: 0 36px; 14 | } 15 | 16 | .navButton { 17 | border: none; 18 | background: none; 19 | color: white; 20 | padding: 16px 0px; 21 | margin: 0px 32px; 22 | cursor: pointer; 23 | border-bottom: 1px solid #fff; 24 | font-size: 16px; 25 | font-family: "Nunito", sans-serif; 26 | font-weight: 500; 27 | } 28 | 29 | .main { 30 | padding: 46px 64px; 31 | } 32 | 33 | .heading { 34 | font-size: 24px; 35 | font-weight: 500; 36 | } 37 | 38 | .subHeading { 39 | font-size: 16px; 40 | font-weight: 400; 41 | color: rgb(255 255 255 / 70%); 42 | display: block; 43 | margin-bottom: 48px; 44 | } 45 | 46 | .grid { 47 | display: flex; 48 | gap: 32px; 49 | } 50 | 51 | .box { 52 | background: white; 53 | box-shadow: 0px 12px 24px rgba(0, 0, 0, 0.08); 54 | border-radius: 16px; 55 | height: 372px; 56 | width: 416px; 57 | } 58 | -------------------------------------------------------------------------------- /examples/oasis/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import {UserButton} from "@clerk/nextjs"; 2 | import NakedLogo from "../assets/svg/naked-logo.svg"; 3 | 4 | import styles from "./Dashboard.module.css"; 5 | 6 | export function Dashboard() { 7 | return ( 8 |
9 |
10 |
11 | 12 | 13 |
14 | 17 |
18 |
19 | Home 20 | Welcome to your application 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /examples/oasis/components/GithubLink.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: "Inter", sans-serif; 3 | font-size: 0.75rem; 4 | font-weight: 400; 5 | line-height: 1rem; 6 | letter-spacing: 0em; 7 | text-align: left; 8 | 9 | display: flex; 10 | 11 | box-shadow: 0px 24px 48px 0px #00000029; 12 | border-radius: 6px; 13 | 14 | height: 40px; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 0 1.5rem; 18 | background-color: white; 19 | } 20 | 21 | .logo { 22 | position: relative; 23 | top: 2px; 24 | min-width: 56px; 25 | } 26 | 27 | .label { 28 | font-family: "Inter", sans-serif; 29 | font-size: 0.75rem; 30 | font-weight: 400; 31 | line-height: 1rem; 32 | letter-spacing: 0em; 33 | text-align: left; 34 | margin: 0 1.5rem; 35 | color: black; 36 | } 37 | 38 | .rightLink { 39 | color: #335bf1; 40 | min-width: 100px; 41 | } 42 | 43 | .rightLink a:link { 44 | text-decoration: none; 45 | color: #335bf1; 46 | } 47 | 48 | .rightLink a:visited { 49 | text-decoration: none; 50 | color: #335bf1; 51 | } 52 | 53 | .rightLink a:hover { 54 | text-decoration: underline; 55 | color: #335bf1; 56 | } 57 | 58 | .rightLink a:active { 59 | text-decoration: underline; 60 | color: #335bf1; 61 | } 62 | 63 | .rightArrow { 64 | position: relative; 65 | top: 2px; 66 | left: 3px; 67 | } 68 | -------------------------------------------------------------------------------- /examples/oasis/components/GithubLink.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./GithubLink.module.css"; 2 | import Image from "next/image"; 3 | 4 | type GithubLinkProps = { 5 | label: string; 6 | repoLink: string; 7 | }; 8 | export const GithubLink = (props: GithubLinkProps) => ( 9 | <> 10 |
24 | 25 | ); 26 | -------------------------------------------------------------------------------- /examples/oasis/components/Home.module.css: -------------------------------------------------------------------------------- 1 | .text { 2 | font-weight: 600; 3 | color: #fff; 4 | margin: 0; 5 | } 6 | -------------------------------------------------------------------------------- /examples/oasis/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Button } from "./Button"; 3 | import { Card } from "./layout/Card"; 4 | import { CustomLink } from "./CustomLink"; 5 | import LogoIcon from "../assets/svg/logo.svg"; 6 | 7 | import styles from "./Home.module.css"; 8 | 9 | export function Home() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 |

17 | Already have an account?{" "} 18 | Sign In 19 |

20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/oasis/components/Input.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | width: 100%; 3 | text-align: center; 4 | padding: 16px; 5 | background: rgba(255, 255, 255, 0.1); 6 | border-radius: 8px; 7 | border: none; 8 | font-size: 20px; 9 | line-height: 24px; 10 | color: #fff; 11 | margin-bottom: 24px; 12 | } 13 | 14 | .input:focus, 15 | .input:active { 16 | border: none; 17 | outline: none; 18 | } 19 | 20 | .helperText { 21 | display: block; 22 | align-self: flex-start; 23 | font-weight: 600; 24 | } 25 | 26 | .errorText { 27 | display: block; 28 | color: #7431d7; 29 | align-self: center; 30 | font-weight: 600; 31 | margin-bottom: 12px; 32 | } 33 | -------------------------------------------------------------------------------- /examples/oasis/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Input.module.css"; 3 | 4 | const Input = React.forwardRef< 5 | HTMLInputElement, 6 | { 7 | helperText?: string; 8 | errorText?: string; 9 | onPaste?: React.ClipboardEventHandler; 10 | autoFocus?: boolean; 11 | } 12 | >(({ autoFocus = true, helperText, errorText, onPaste, ...rest }, ref) => { 13 | return ( 14 | <> 15 | {helperText && {helperText}} 16 | 23 | {errorText && {errorText}} 24 | 25 | ); 26 | }); 27 | 28 | Input.displayName = "Input"; 29 | 30 | export { Input }; 31 | -------------------------------------------------------------------------------- /examples/oasis/components/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import formStyles from "./layout/FormLayout.module.css"; 2 | import { useClerk, useSignIn, withClerk } from "@clerk/nextjs"; 3 | import { useState } from "react"; 4 | import { useForm } from "react-hook-form"; 5 | import { useRouter } from "next/router"; 6 | import { Button } from "./Button"; 7 | import { Input } from "./Input"; 8 | import { FormLayout } from "./layout/FormLayout"; 9 | import { Title } from "./Title"; 10 | import { parseError, APIResponseError } from "../utils/errors"; 11 | 12 | const SIMPLE_REGEX_PATTERN = /^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; 13 | 14 | type SignInInputs = { 15 | email: string; 16 | code: string; 17 | }; 18 | 19 | enum FormSteps { 20 | EMAIL, 21 | CODE, 22 | } 23 | 24 | function SignIn() { 25 | const router = useRouter(); 26 | const { isLoaded, signIn } = useSignIn(); 27 | const clerk = useClerk(); 28 | 29 | const [formStep, setFormStep] = useState(FormSteps.EMAIL); 30 | const { 31 | register, 32 | getValues, 33 | trigger, 34 | setError, 35 | clearErrors, 36 | handleSubmit, 37 | formState: { errors, isSubmitting }, 38 | } = useForm({ mode: "all" }); 39 | 40 | if (!isLoaded) { 41 | return null; 42 | } 43 | 44 | const sendOtp = async function () { 45 | const emailAddress = getValues("email"); 46 | const signInAttempt = await signIn.create({ 47 | identifier: emailAddress, 48 | }); 49 | 50 | const emailCodeFactor = signInAttempt.supportedFirstFactors.find( 51 | (factor) => factor.strategy === "email_code" 52 | ); 53 | 54 | await signInAttempt.prepareFirstFactor({ 55 | strategy: "email_code", 56 | // @ts-ignore 57 | email_address_id: emailCodeFactor.email_address_id, 58 | }); 59 | }; 60 | 61 | const verifyEmail = async function () { 62 | try { 63 | clearErrors(); 64 | await sendOtp(); 65 | setFormStep((formStep) => formStep + 1); 66 | } catch (err) { 67 | setError("email", { 68 | type: "manual", 69 | message: parseError(err as APIResponseError), 70 | }); 71 | } 72 | }; 73 | 74 | const verifyOtp = async function () { 75 | const otp = getValues("code"); 76 | let signUpAttempt; 77 | 78 | try { 79 | signUpAttempt = await signIn.attemptFirstFactor({ 80 | strategy: "email_code", 81 | code: otp, 82 | }); 83 | } catch (err) { 84 | setError("code", { 85 | type: "manual", 86 | message: parseError(err as APIResponseError), 87 | }); 88 | } 89 | 90 | if (signUpAttempt?.status === "complete") { 91 | clerk.setSession(signUpAttempt.createdSessionId, () => 92 | router.push("/dashboard") 93 | ); 94 | } 95 | }; 96 | 97 | const onSubmit = () => { 98 | switch (formStep) { 99 | case FormSteps.EMAIL: 100 | return verifyEmail(); 101 | case FormSteps.CODE: 102 | return verifyOtp(); 103 | } 104 | }; 105 | 106 | return ( 107 | 108 |
109 |
110 | {formStep === FormSteps.EMAIL && ( 111 | <> 112 | Sign in 113 | 121 | 128 | 129 | )} 130 | {formStep === FormSteps.CODE && ( 131 | <> 132 | Enter the confirmation code 133 | 134 | A 6-digit code was just sent to {getValues("email")} 135 | 136 | await trigger("code")} 144 | /> 145 | 148 | 149 | )} 150 |
151 |
152 |
153 | ); 154 | } 155 | 156 | export const SignInWithClerk = withClerk(SignIn); 157 | -------------------------------------------------------------------------------- /examples/oasis/components/Title.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-family: "Nunito", sans-serif; 3 | font-style: normal; 4 | font-weight: bold; 5 | font-size: 32px; 6 | line-height: 44px; 7 | text-align: center; 8 | margin-bottom: 48px; 9 | } 10 | -------------------------------------------------------------------------------- /examples/oasis/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Title.module.css"; 2 | 3 | export function Title({ children }: { children: React.ReactNode }) { 4 | return {children}; 5 | } 6 | -------------------------------------------------------------------------------- /examples/oasis/components/layout/Card.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | width: 524px; 3 | display: flex; 4 | align-items: center; 5 | flex-direction: column; 6 | } 7 | -------------------------------------------------------------------------------- /examples/oasis/components/layout/Card.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Card.module.css"; 2 | 3 | export function Card({ 4 | children, 5 | className, 6 | style, 7 | }: { 8 | children: React.ReactNode; 9 | className?: string; 10 | style?: React.CSSProperties; 11 | }) { 12 | return ( 13 |
14 | {children} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/oasis/components/layout/FormLayout.module.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | margin: 128px 0px 96px 0; 3 | display: flex; 4 | justify-content: center; 5 | } 6 | 7 | .text { 8 | padding-top: 48px; 9 | font-weight: 600; 10 | color: #fff; 11 | margin: 0; 12 | text-align: center; 13 | } 14 | 15 | .fields { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | min-width: 524px; 20 | } 21 | 22 | .sub { 23 | margin-top: -36px; 24 | font-weight: 600; 25 | text-align: center; 26 | margin-bottom: 48px; 27 | } 28 | -------------------------------------------------------------------------------- /examples/oasis/components/layout/FormLayout.tsx: -------------------------------------------------------------------------------- 1 | import LogoIcon from "../../assets/svg/logo.svg"; 2 | import { CustomLink } from "../CustomLink"; 3 | import styles from "./FormLayout.module.css"; 4 | 5 | type FormType = "sign-up" | "sign-in" | "create-profile"; 6 | 7 | export function FormLayout({ 8 | children, 9 | type, 10 | }: { 11 | children: React.ReactNode; 12 | type: FormType; 13 | }) { 14 | return ( 15 |
16 |
17 | 18 |
19 | {children} 20 | {type === "sign-in" && } 21 | {type === "sign-up" && } 22 |
23 | ); 24 | } 25 | 26 | const SignUpLink = () => ( 27 |

28 | No account? Sign up 29 |

30 | ); 31 | 32 | const SignInLink = () => ( 33 |

34 | Already have an account? Sign in 35 |

36 | ); 37 | -------------------------------------------------------------------------------- /examples/oasis/components/layout/MainLayout.module.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | height: 100vh; 3 | background: linear-gradient(180deg, #ff4265 0%, #ff885b 100%); 4 | display: flex; 5 | justify-content: center; 6 | } 7 | -------------------------------------------------------------------------------- /examples/oasis/components/layout/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./MainLayout.module.css"; 2 | 3 | export function MainLayout({ children }: { children: React.ReactNode }) { 4 | return
{children}
; 5 | } 6 | -------------------------------------------------------------------------------- /examples/oasis/components/profile/Profile.module.css: -------------------------------------------------------------------------------- 1 | .fileInput { 2 | display: none; 3 | } 4 | 5 | .fileButton { 6 | padding: 32px; 7 | border: none; 8 | border-radius: 48px; 9 | background: rgba(255, 255, 255, 0.1); 10 | cursor: pointer; 11 | } 12 | 13 | .skip { 14 | background: none; 15 | border: none; 16 | font-size: 16px; 17 | color: #fff; 18 | padding: 16px 32px; 19 | cursor: pointer; 20 | margin-top: 16px; 21 | } 22 | 23 | .profileImg { 24 | border-radius: 48px; 25 | max-height: 96px; 26 | } 27 | -------------------------------------------------------------------------------- /examples/oasis/components/profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { FormLayout } from "../layout/FormLayout"; 3 | import { ProfileCompleteStep } from "./ProfileCompleteStep"; 4 | import { ProfileUploadPhotoStep } from "./ProfileUploadPhotoStep"; 5 | 6 | enum FormSteps { 7 | START, 8 | PHOTO, 9 | } 10 | 11 | export function Profile() { 12 | const [formStep, setFormStep] = useState(FormSteps.START); 13 | const gotoNextStep = () => setFormStep((formStep) => formStep + 1); 14 | 15 | return ( 16 | 17 | {(() => { 18 | switch (formStep) { 19 | case FormSteps.START: 20 | return ; 21 | case FormSteps.PHOTO: 22 | return ; 23 | } 24 | })()} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/oasis/components/profile/ProfileCompleteStep.tsx: -------------------------------------------------------------------------------- 1 | import formStyles from "../layout/FormLayout.module.css"; 2 | import styles from "./Profile.module.css"; 3 | import React from "react"; 4 | import { Button } from "../Button"; 5 | import { Title } from "../Title"; 6 | import { useRouter } from "next/router"; 7 | 8 | type ProfileCompleteStepProps = { 9 | onDone: () => void; 10 | }; 11 | 12 | export function ProfileCompleteStep({ onDone }: ProfileCompleteStepProps) { 13 | const router = useRouter(); 14 | return ( 15 |
16 | Account created 17 |

Fill out your profile to finish setting up your account.

18 | 21 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /examples/oasis/components/profile/ProfileUploadPhotoStep.tsx: -------------------------------------------------------------------------------- 1 | import formStyles from "../layout/FormLayout.module.css"; 2 | import styles from "./Profile.module.css"; 3 | import UploadIcon from "../../assets/svg/upload.svg"; 4 | import React, {useRef, useState} from "react"; 5 | import {useRouter} from "next/router"; 6 | import {useUser} from "@clerk/nextjs"; 7 | import {useForm} from "react-hook-form"; 8 | import {Button} from "../Button"; 9 | import {Title} from "../Title"; 10 | 11 | export function ProfileUploadPhotoStep() { 12 | const { user } = useUser(); 13 | const router = useRouter(); 14 | const {register, getValues, handleSubmit} = useForm<{ photo?: File[] }>({ 15 | mode: "all", 16 | }); 17 | const [photoSrc, setPhotoSrc] = useState(""); 18 | const fileRef = useRef(null); 19 | 20 | const {ref: fileUploadRef, onChange: onFileChangeHookForm} = 21 | register("photo"); 22 | 23 | const promptForFile = () => { 24 | fileRef.current?.click(); 25 | }; 26 | 27 | const onFileChange = (e: React.ChangeEvent) => { 28 | const files = e.currentTarget.files ?? []; 29 | if (files[0]) { 30 | setPhotoSrc(URL.createObjectURL(files[0])); 31 | } 32 | }; 33 | 34 | const onSubmit = async () => { 35 | const photo = getValues("photo")?.[0]; 36 | if (photo) { 37 | await user?.setProfileImage({file: photo}); 38 | } 39 | 40 | router.push("/dashboard"); 41 | }; 42 | 43 | return ( 44 |
45 | Upload a photo 46 | { 48 | onFileChangeHookForm(e); 49 | onFileChange(e); 50 | }} 51 | name="photo" 52 | ref={(e) => { 53 | fileUploadRef(e); 54 | fileRef.current = e; 55 | }} 56 | type="file" 57 | accept="image/jpeg, 58 | image/png, 59 | image/gif, 60 | image/webp" 61 | className={styles.fileInput} 62 | /> 63 | {photoSrc ? ( 64 | profile 65 | ) : ( 66 | 74 | )} 75 | 78 | 86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /examples/oasis/components/profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Profile"; 2 | -------------------------------------------------------------------------------- /examples/oasis/components/signUp/SignUp.module.css: -------------------------------------------------------------------------------- 1 | .text { 2 | font-weight: 600; 3 | color: #fff; 4 | margin: 0; 5 | } 6 | -------------------------------------------------------------------------------- /examples/oasis/components/signUp/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { FormLayout } from "../layout/FormLayout"; 3 | import { SignUpEmailStep } from "./SignUpEmailStep"; 4 | import { SignUpCodeStep } from "./SignUpCodeStep"; 5 | import { SignUpFirstNameStep } from "./SignUpFirstNameStep"; 6 | import { SignUpLastNameStep } from "./SignUpLastNameStep"; 7 | import { SignUpUsernameStep } from "./SignUpUsernameStep"; 8 | 9 | enum FormSteps { 10 | EMAIL, 11 | CODE, 12 | FIRST_NAME, 13 | LAST_NAME, 14 | USERNAME, 15 | } 16 | 17 | function SignUp() { 18 | const [formStep, setFormStep] = useState(FormSteps.EMAIL); 19 | const gotoNextStep = () => setFormStep((formStep) => formStep + 1); 20 | 21 | return ( 22 | 23 | {(() => { 24 | switch (formStep) { 25 | case FormSteps.EMAIL: 26 | return ; 27 | case FormSteps.CODE: 28 | return ; 29 | case FormSteps.FIRST_NAME: 30 | return ; 31 | case FormSteps.LAST_NAME: 32 | return ; 33 | case FormSteps.USERNAME: 34 | return ; 35 | } 36 | })()} 37 | 38 | ); 39 | } 40 | 41 | export const SignUpWithClerk = SignUp; 42 | -------------------------------------------------------------------------------- /examples/oasis/components/signUp/SignUpCodeStep.tsx: -------------------------------------------------------------------------------- 1 | import formStyles from "../layout/FormLayout.module.css"; 2 | import { useSignUp } from "@clerk/nextjs"; 3 | import { useForm } from "react-hook-form"; 4 | import { Button } from "../Button"; 5 | import { Input } from "../Input"; 6 | import { Title } from "../Title"; 7 | import { parseError, APIResponseError } from "../../utils/errors"; 8 | 9 | type SignUpCodeStepProps = { 10 | onDone: () => void; 11 | }; 12 | 13 | export function SignUpCodeStep({ onDone }: SignUpCodeStepProps) { 14 | const { signUp, isLoaded } = useSignUp(); 15 | 16 | const { 17 | register, 18 | getValues, 19 | trigger, 20 | setError, 21 | clearErrors, 22 | handleSubmit, 23 | formState: { errors, isSubmitting }, 24 | } = useForm<{ code: string }>({ mode: "all" }); 25 | 26 | if (!isLoaded) { 27 | return null; 28 | } 29 | 30 | const verifyOtp = async function () { 31 | const otp = getValues("code"); 32 | try { 33 | clearErrors(); 34 | const signUpAttempt = await signUp.attemptEmailAddressVerification({ 35 | code: otp, 36 | }); 37 | if (signUpAttempt.verifications.emailAddress.status === "verified") { 38 | onDone(); 39 | } 40 | } catch (err) { 41 | setError("code", { 42 | type: "manual", 43 | message: parseError(err as APIResponseError), 44 | }); 45 | } 46 | }; 47 | 48 | return ( 49 |
50 | Enter the confirmation code 51 | 52 | A 6-digit code was just sent to {signUp.emailAddress} 53 |
54 |
55 | await trigger("code")} 63 | /> 64 | 69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /examples/oasis/components/signUp/SignUpEmailStep.tsx: -------------------------------------------------------------------------------- 1 | import formStyles from "../layout/FormLayout.module.css"; 2 | import { useForm } from "react-hook-form"; 3 | import { useSignUp } from "@clerk/nextjs"; 4 | import { Button } from "../Button"; 5 | import { Input } from "../Input"; 6 | import { Title } from "../Title"; 7 | import { parseError, APIResponseError } from "../../utils/errors"; 8 | import React from "react"; 9 | 10 | const SIMPLE_REGEX_PATTERN = /^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; 11 | 12 | type SignUpEmailStepProps = { 13 | onDone: () => void; 14 | }; 15 | 16 | export function SignUpEmailStep({ onDone }: SignUpEmailStepProps) { 17 | const { signUp, isLoaded } = useSignUp(); 18 | 19 | const { 20 | register, 21 | getValues, 22 | setError, 23 | clearErrors, 24 | handleSubmit, 25 | formState: { errors, isSubmitting }, 26 | } = useForm<{ email: string }>({ mode: "all" }); 27 | 28 | if (!isLoaded) { 29 | return null; 30 | } 31 | 32 | const sendClerkOtp = async function () { 33 | const emailAddress = getValues("email"); 34 | const signUpAttempt = await signUp.create({ 35 | emailAddress, 36 | }); 37 | await signUpAttempt.prepareEmailAddressVerification({ 38 | strategy: "email_code", 39 | }); 40 | }; 41 | 42 | const verifyEmail = async function () { 43 | try { 44 | clearErrors(); 45 | await sendClerkOtp(); 46 | onDone(); 47 | } catch (err) { 48 | setError("email", { 49 | type: "manual", 50 | message: parseError(err as APIResponseError), 51 | }); 52 | } 53 | }; 54 | 55 | return ( 56 |
57 | What’s your email address? 58 | 65 | 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /examples/oasis/components/signUp/SignUpFirstNameStep.tsx: -------------------------------------------------------------------------------- 1 | import formStyles from "../layout/FormLayout.module.css"; 2 | import { useForm } from "react-hook-form"; 3 | import { useSignUp } from "@clerk/nextjs"; 4 | import { Button } from "../Button"; 5 | import { Input } from "../Input"; 6 | import { Title } from "../Title"; 7 | import { APIResponseError, parseError } from "../../utils/errors"; 8 | 9 | type SignUpFirstNameStepProps = { 10 | onDone: () => void; 11 | }; 12 | 13 | export function SignUpFirstNameStep({ onDone }: SignUpFirstNameStepProps) { 14 | const { signUp, isLoaded } = useSignUp(); 15 | 16 | const { 17 | register, 18 | getValues, 19 | clearErrors, 20 | setError, 21 | formState: { errors, isSubmitting }, 22 | handleSubmit, 23 | } = useForm<{ firstName: string }>({ mode: "all" }); 24 | 25 | if (!isLoaded) { 26 | return null; 27 | } 28 | 29 | const save = async () => { 30 | try { 31 | clearErrors(); 32 | await signUp.update({ firstName: getValues("firstName") }); 33 | onDone(); 34 | } catch (err) { 35 | setError("firstName", { 36 | type: "manual", 37 | message: parseError(err as APIResponseError), 38 | }); 39 | } 40 | }; 41 | 42 | return ( 43 |
44 | What’s your first name? 45 | 49 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /examples/oasis/components/signUp/SignUpLastNameStep.tsx: -------------------------------------------------------------------------------- 1 | import formStyles from "../layout/FormLayout.module.css"; 2 | import { useForm } from "react-hook-form"; 3 | import { useSignUp } from "@clerk/nextjs"; 4 | import { Button } from "../Button"; 5 | import { Input } from "../Input"; 6 | import { Title } from "../Title"; 7 | import { APIResponseError, parseError } from "../../utils/errors"; 8 | 9 | type SignUpLastNAmeStepProps = { 10 | onDone: () => void; 11 | }; 12 | 13 | export function SignUpLastNameStep({ onDone }: SignUpLastNAmeStepProps) { 14 | const { signUp, isLoaded } = useSignUp(); 15 | 16 | const { 17 | register, 18 | getValues, 19 | setError, 20 | clearErrors, 21 | handleSubmit, 22 | formState: { errors, isSubmitting }, 23 | } = useForm<{ lastName: string }>({ mode: "all" }); 24 | 25 | if (!isLoaded) { 26 | return null; 27 | } 28 | 29 | const save = async () => { 30 | try { 31 | clearErrors(); 32 | await signUp.update({ lastName: getValues("lastName") }); 33 | onDone(); 34 | } catch (err) { 35 | setError("lastName", { 36 | type: "manual", 37 | message: parseError(err as APIResponseError), 38 | }); 39 | } 40 | }; 41 | 42 | return ( 43 |
44 | What’s your last name? 45 | 49 | 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /examples/oasis/components/signUp/SignUpUsernameStep.tsx: -------------------------------------------------------------------------------- 1 | import formStyles from "../layout/FormLayout.module.css"; 2 | import { useForm } from "react-hook-form"; 3 | import { useSignUp } from "@clerk/nextjs"; 4 | import { Button } from "../Button"; 5 | import { Input } from "../Input"; 6 | import { Title } from "../Title"; 7 | import { APIResponseError, parseError } from "../../utils/errors"; 8 | import router from "next/router"; 9 | 10 | export function SignUpUsernameStep() { 11 | const { signUp, isLoaded, setSession } = useSignUp(); 12 | 13 | const { 14 | register, 15 | getValues, 16 | setError, 17 | clearErrors, 18 | handleSubmit, 19 | formState: { errors, isSubmitting }, 20 | } = useForm<{ username: string }>({ mode: "all" }); 21 | 22 | if (!isLoaded) { 23 | return null; 24 | } 25 | 26 | const save = async () => { 27 | try { 28 | clearErrors(); 29 | const completeSignUp = await signUp.update({ 30 | username: getValues("username"), 31 | }); 32 | if (completeSignUp.status === "complete") { 33 | await setSession(completeSignUp.createdSessionId, () => { 34 | return router.push("/create-profile"); 35 | }); 36 | } 37 | } catch (err) { 38 | setError("username", { 39 | type: "manual", 40 | message: parseError(err as APIResponseError), 41 | }); 42 | } 43 | }; 44 | 45 | return ( 46 |
47 | Create your username 48 | 56 | 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /examples/oasis/components/signUp/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SignUp"; 2 | -------------------------------------------------------------------------------- /examples/oasis/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/oasis/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | webpack(config) { 5 | config.module.rules.push({ 6 | test: /\.svg$/, 7 | use: ["@svgr/webpack"] 8 | }); 9 | return config; 10 | } 11 | } -------------------------------------------------------------------------------- /examples/oasis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clerk-oasis-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^3.7.0", 13 | "next": "12.2.2", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "react-hook-form": "^7.33.1" 17 | }, 18 | "devDependencies": { 19 | "@svgr/webpack": "^5.5.0", 20 | "@types/react": "18.0.15", 21 | "eslint": "8.0.1", 22 | "eslint-config-next": "12.2.2", 23 | "typescript": "4.7.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/oasis/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/index.css"; 2 | import type { AppProps } from "next/app"; 3 | import { useRouter } from "next/router"; 4 | import { 5 | ClerkProvider, 6 | RedirectToSignUp, 7 | SignedIn, 8 | SignedOut, 9 | } from "@clerk/nextjs"; 10 | import { GithubLink } from "../components/GithubLink"; 11 | import { MainLayout } from "../components/layout/MainLayout"; 12 | 13 | function MyApp({ Component, pageProps }: AppProps) { 14 | const { pathname } = useRouter(); 15 | 16 | const publicPages = ["/", "/sign-in/[[...index]]", "/sign-up/[[...index]]"]; 17 | 18 | console.log({pathname}) 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | {publicPages.includes(pathname) ? ( 28 | 29 | ) : ( 30 | 31 | )} 32 | 33 | 34 |
35 | 39 |
40 |
41 | ); 42 | } 43 | export default MyApp; 44 | -------------------------------------------------------------------------------- /examples/oasis/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default MyDocument; 24 | -------------------------------------------------------------------------------- /examples/oasis/pages/create-profile.tsx: -------------------------------------------------------------------------------- 1 | import { Profile } from "../components/profile"; 2 | import type { NextPage } from "next"; 3 | 4 | const CreateProfilePage: NextPage = () => { 5 | return ; 6 | }; 7 | 8 | export default CreateProfilePage; 9 | -------------------------------------------------------------------------------- /examples/oasis/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Dashboard } from "../components/Dashboard"; 2 | import type { NextPage } from "next"; 3 | 4 | const Index: NextPage = () => { 5 | return ; 6 | }; 7 | 8 | export default Index; 9 | -------------------------------------------------------------------------------- /examples/oasis/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Home } from "../components/Home"; 2 | import type { NextPage } from "next"; 3 | 4 | const Index: NextPage = () => { 5 | return ; 6 | }; 7 | 8 | export default Index; 9 | -------------------------------------------------------------------------------- /examples/oasis/pages/sign-in/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import { SignInWithClerk } from "../../components/SignIn"; 2 | 3 | const SigninPage = () => { 4 | return ; 5 | }; 6 | 7 | export default SigninPage; 8 | -------------------------------------------------------------------------------- /examples/oasis/pages/sign-up/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import { SignUpWithClerk } from "../../components/signUp"; 2 | 3 | const SignUpFormPage = () => { 4 | return ; 5 | }; 6 | 7 | export default SignUpFormPage; 8 | -------------------------------------------------------------------------------- /examples/oasis/public/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/oasis/public/clerk.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/oasis/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/examples/oasis/public/favicon.png -------------------------------------------------------------------------------- /examples/oasis/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | color: #fff; 4 | font-family: "Nunito", sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 11 | monospace; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | footer { 19 | position: fixed; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | margin: 20px; 27 | } 28 | -------------------------------------------------------------------------------- /examples/oasis/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/oasis/svg/naked-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/oasis/svg/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/oasis/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/oasis/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import type { ClerkAPIError } from "@clerk/types"; 2 | 3 | export interface APIResponseError { 4 | errors: ClerkAPIError[]; 5 | } 6 | 7 | export function parseError(err: APIResponseError): string { 8 | if (!err) { 9 | return ""; 10 | } 11 | 12 | if (err.errors) { 13 | return err.errors[0].longMessage || ""; 14 | } 15 | 16 | throw err; 17 | } 18 | -------------------------------------------------------------------------------- /examples/simple/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/simple/.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 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Clerk Simple Starter 2 | 3 | ![Preview](../../docs/simple.png) 4 | 5 | This Next.js project demonstrates how to use [@clerk/clerk-nextjs](https://clerk.dev/docs/quickstarts/get-started-with-nextjs?utm_source=github&utm_medium=starters&utm_campaign=simple) together with Clerk Components showcasing Clerk's capabilities. 6 | 7 | [![Open in VS Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/clerkinc/clerk-nextjs-examples) 8 | 9 | ## Live demo 10 | 11 | https://nextjs.simple.clerk.app/ 12 | 13 | ## Getting Started 14 | 15 | ### 1. Setup environment variables 16 | 17 | First, you need to set two environment variables for Clerk's SDK. Go to the API Keys page and start by copying the Frontend API key. Then, add it a .env.local file in your application root, with the name `NEXT_PUBLIC_CLERK_FRONTEND_API`: 18 | 19 | ```bash 20 | # Replace [frontend-api-key] with your actual Frontend API key 21 | echo "NEXT_PUBLIC_CLERK_FRONTEND_API=[frontend-api-key]" >> .env.local 22 | ``` 23 | 24 | Next, go back to the API Keys page and copy the Backend API key. Then, add it in the same .env.local file, with the name `CLERK_API_KEY`: 25 | 26 | ```bash 27 | # Replace [backend-api-key] with your actual Backend API key 28 | echo "CLERK_API_KEY=[backend-api-key]" >> .env.local 29 | 30 | ``` 31 | 32 | ### 2. Run the development server 33 | 34 | ```bash 35 | npm run dev 36 | ``` 37 | 38 | Lastly, open [http://localhost:3000](http://localhost:3000) with your browser. 39 | 40 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 41 | 42 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 43 | 44 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 45 | 46 | ## Learn More 47 | 48 | To learn more about Next.js, take a look at the following resources: 49 | 50 | - [Next.js authentication with Clerk](https://clerk.dev/docs/quickstarts/get-started-with-nextjs) - Add secure, beautiful, and fast authentication to Next.js with Clerk. 51 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 52 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 53 | 54 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 55 | 56 | ## Deploy on Vercel 57 | 58 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 59 | 60 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 61 | -------------------------------------------------------------------------------- /examples/simple/components/Header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | height: 100px; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | } 7 | 8 | .headerItem { 9 | margin: 0 3rem; 10 | } 11 | 12 | .signInButton { 13 | cursor: pointer; 14 | padding: 1rem; 15 | background-color: #335bf1; 16 | color: white; 17 | border-radius: 0.375rem; 18 | } 19 | 20 | .signInButton:hover { 21 | opacity: 0.9; 22 | } 23 | 24 | .homeButton { 25 | cursor: pointer; 26 | padding: 1rem; 27 | color: #335bf1; 28 | border-radius: 0.375rem; 29 | } 30 | -------------------------------------------------------------------------------- /examples/simple/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Header.module.css"; 2 | import type { NextPage } from "next"; 3 | import { useClerk, SignedIn, SignedOut, UserButton } from "@clerk/nextjs"; 4 | import Link from "next/link"; 5 | 6 | // Header component using & . 7 | // 8 | // The SignedIn and SignedOut components are used to control rendering depending 9 | // on whether or not a visitor is signed in. 10 | // 11 | // https://clerk.dev/docs/component-reference/signed-in 12 | // https://clerk.dev/docs/component-reference/signed-out 13 | 14 | const Header: NextPage = () => { 15 | const { openSignIn } = useClerk(); 16 | 17 | return ( 18 |
19 |
20 | 21 | Simple 22 | 23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 | 35 | 36 |
37 | ); 38 | }; 39 | 40 | export default Header; 41 | -------------------------------------------------------------------------------- /examples/simple/components/clerk/GithubLink.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: "Inter", sans-serif; 3 | font-size: 0.75rem; 4 | font-weight: 400; 5 | line-height: 1rem; 6 | letter-spacing: 0em; 7 | text-align: left; 8 | 9 | display: flex; 10 | 11 | box-shadow: 0px 24px 48px 0px #00000029; 12 | border-radius: 6px; 13 | 14 | height: 40px; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 0 1.5rem; 18 | background-color: white; 19 | } 20 | 21 | .logo { 22 | position: relative; 23 | top: 2px; 24 | min-width: 56px; 25 | } 26 | 27 | .label { 28 | font-family: "Inter", sans-serif; 29 | font-size: 0.75rem; 30 | font-weight: 400; 31 | line-height: 1rem; 32 | letter-spacing: 0em; 33 | text-align: left; 34 | margin: 0 1.5rem; 35 | } 36 | 37 | .rightLink { 38 | color: #335bf1; 39 | min-width: 100px; 40 | } 41 | 42 | .rightLink a:link { 43 | text-decoration: none; 44 | color: #335bf1; 45 | } 46 | 47 | .rightLink a:visited { 48 | text-decoration: none; 49 | color: #335bf1; 50 | } 51 | 52 | .rightLink a:hover { 53 | text-decoration: underline; 54 | color: #335bf1; 55 | } 56 | 57 | .rightLink a:active { 58 | text-decoration: underline; 59 | color: #335bf1; 60 | } 61 | 62 | .rightArrow { 63 | position: relative; 64 | top: 2px; 65 | left: 3px; 66 | } 67 | -------------------------------------------------------------------------------- /examples/simple/components/clerk/GithubLink.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./GithubLink.module.css"; 2 | import Image from "next/image"; 3 | 4 | type GithubLinkProps = { 5 | label: string; 6 | repoLink: string; 7 | }; 8 | const GithubLink = (props: GithubLinkProps) => ( 9 | <> 10 |
11 | 12 | clerk 13 | 14 |
{props.label}
15 | 23 |
24 | 25 | ); 26 | 27 | export default GithubLink; 28 | -------------------------------------------------------------------------------- /examples/simple/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/simple/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-plain", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^3.7.0", 13 | "next": "12.2.2", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "18.0.15", 19 | "eslint": "8.0.1", 20 | "eslint-config-next": "12.2.2", 21 | "typescript": "4.7.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/simple/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import GithubLink from "../components/clerk/GithubLink"; 3 | import Header from "../components/Header"; 4 | import type {AppProps} from "next/app"; 5 | import {ClerkLoaded, ClerkProvider, SignedIn, SignedOut} from "@clerk/nextjs"; 6 | import {useRouter} from "next/router"; 7 | 8 | /** 9 | * List pages you want to be publicly accessible, or leave empty if 10 | * every page requires authentication. Use this naming strategy: 11 | * "/" for pages/index.js 12 | * "/foo" for pages/foo/index.js 13 | * "/foo/bar" for pages/foo/bar.js 14 | * "/foo/[...bar]" for pages/foo/[...bar].js 15 | */ 16 | const publicPages = ["/"]; 17 | 18 | function MyApp({ Component, pageProps }: AppProps) { 19 | const router = useRouter(); 20 | 21 | return ( 22 | 23 | 24 |
25 | {publicPages.includes(router.pathname) ? ( 26 |
27 | 28 |
29 | ) : ( 30 | <> 31 | 32 | 33 | 34 | 35 |
36 |

You need to be signed in to access this page.

37 |
38 |
39 | 40 | )} 41 | 42 | {/* footer */} 43 |
44 | 48 |
49 | 50 | 51 | ); 52 | } 53 | export default MyApp; 54 | -------------------------------------------------------------------------------- /examples/simple/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default MyDocument; 24 | -------------------------------------------------------------------------------- /examples/simple/pages/api/hello-js.js: -------------------------------------------------------------------------------- 1 | import {withAuth} from "@clerk/nextjs/api"; 2 | 3 | export default withAuth((req, res) => { 4 | res.statusCode = 200; 5 | 6 | if (req.session) { 7 | res.write(`signed in as user: ${req.session.userId}`); 8 | } else { 9 | res.json("not signed in"); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /examples/simple/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { withAuth, WithAuthProp } from "@clerk/nextjs/api"; 3 | 4 | export default withAuth( 5 | (req: WithAuthProp, res: NextApiResponse) => { 6 | res.statusCode = 200; 7 | if (req.auth) { 8 | res.write(`signed in as user: ${req.auth.userId}`); 9 | } else { 10 | res.json("not signed in"); 11 | } 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /examples/simple/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useClerk, useUser, SignedOut, SignedIn } from "@clerk/nextjs"; 2 | import React from "react"; 3 | import type { NextPage } from "next"; 4 | import styles from "../styles/Home.module.css"; 5 | import Link from "next/link"; 6 | 7 | const thisRepoLink = 8 | "https://github.com/clerkinc/clerk-nextjs-examples/tree/main/examples/simple"; 9 | 10 | const allExamplesLink = "https://github.com/clerkinc/clerk-nextjs-examples"; 11 | 12 | const Home: NextPage = () => { 13 | return ( 14 |
15 |
16 |

17 | Welcome to Clerk 18 |

19 | 20 |

21 | This is a live demo of Clerk's authentication modals built in{" "} 22 | Next.js. 23 |
24 | Simple and Powerful. 25 |

26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 | ); 37 | }; 38 | 39 | const SignedOutCards = () => { 40 | const { openSignIn, openSignUp } = useClerk(); 41 | return ( 42 | <> 43 | openSignIn()} className={styles.card}> 44 |

Sign in →

45 |

Show the sign in modal

46 |
47 | openSignUp()} className={styles.card}> 48 |

Sign up →

49 |

Show the sign up modal

50 |
51 | 57 |

See the Code →

58 |

All this in just a few lines.

59 |
60 | 66 |

More Examples →

67 |

See what else you can build.

68 |
69 | 70 | ); 71 | }; 72 | 73 | const SignedInCards = () => { 74 | const { user } = useUser(); 75 | 76 | return ( 77 | <> 78 | 79 |

Welcome!

80 |

Signed in as: {user?.primaryEmailAddress!.toString()}

81 |
82 | 83 | 84 |

Go to User Profile →

85 |

Change your password and more

86 |
87 | 88 | 94 |

See the Code →

95 |

All this in just a few lines.

96 |
97 | 103 |

More Examples →

104 |

See what else you can build.

105 |
106 | 107 | ); 108 | }; 109 | 110 | export default Home; 111 | -------------------------------------------------------------------------------- /examples/simple/pages/user/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import { UserProfile } from "@clerk/nextjs"; 2 | 3 | const UserProfilePage = () => ; 4 | 5 | export default UserProfilePage; 6 | -------------------------------------------------------------------------------- /examples/simple/public/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/simple/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/examples/simple/public/favicon.png -------------------------------------------------------------------------------- /examples/simple/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: calc(100vh - 100px); 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .innerContainer { 11 | padding: 2rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | /* justify-content: center; */ 16 | /* align-items: center; */ 17 | } 18 | 19 | .title a { 20 | color: #335bf1; 21 | text-decoration: none; 22 | } 23 | 24 | .title a:hover, 25 | .title a:focus, 26 | .title a:active { 27 | text-decoration: underline; 28 | } 29 | 30 | .title { 31 | margin: 0; 32 | line-height: 1.15; 33 | font-size: 4rem; 34 | } 35 | 36 | .title, 37 | .description { 38 | text-align: center; 39 | } 40 | 41 | .description { 42 | line-height: 1.5; 43 | font-size: 1.5rem; 44 | } 45 | 46 | .grid { 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | flex-wrap: wrap; 51 | max-width: 800px; 52 | margin-top: 1rem; 53 | } 54 | 55 | .card { 56 | cursor: pointer; 57 | margin: 1rem; 58 | padding: 1.5rem; 59 | text-align: left; 60 | color: inherit; 61 | text-decoration: none; 62 | border: 1px solid #eaeaea; 63 | border-radius: 10px; 64 | transition: color 0.15s ease, border-color 0.15s ease; 65 | width: 45%; 66 | } 67 | 68 | .card:hover, 69 | .card:focus, 70 | .card:active { 71 | color: #335bf1; 72 | border-color: #335bf1; 73 | } 74 | 75 | .staticCard { 76 | cursor: pointer; 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | text-decoration: none; 81 | border: 1px solid #335bf1; 82 | color: #335bf1; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | width: 45%; 86 | } 87 | 88 | .card h2, 89 | .staticCard h2 { 90 | margin: 0 0 1rem 0; 91 | font-size: 1.5rem; 92 | } 93 | 94 | .card p, 95 | .staticCard p { 96 | margin: 0; 97 | font-size: 1.25rem; 98 | line-height: 1.5; 99 | } 100 | 101 | @media (max-width: 600px) { 102 | .grid { 103 | width: 100%; 104 | flex-direction: column; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /examples/simple/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | .protected { 19 | width: 100vw; 20 | height: 60vh; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | 26 | footer { 27 | position: fixed; 28 | left: 0; 29 | right: 0; 30 | bottom: 0; 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | margin: 20px; 35 | } 36 | -------------------------------------------------------------------------------- /examples/simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/widget/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_FRONTEND_API= 2 | DATABASE_URL="postgresql://user:password@localhost:5432/db_name?schema=public" 3 | WEBHOOK_SECRET="" 4 | -------------------------------------------------------------------------------- /examples/widget/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/widget/.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 | -------------------------------------------------------------------------------- /examples/widget/README.md: -------------------------------------------------------------------------------- 1 | # Clerk Widget Starter 2 | 3 | ![Preview](../../docs/widget.png) 4 | 5 | This Next.js project demonstrates how to use [@clerk/clerk-nextjs](https://clerk.dev/docs/quickstarts/get-started-with-nextjs?utm_source=github&utm_medium=starters&utm_campaign=widget) together with the core ClerkJS SDK methods to build a custom sign-up and sign-in screen with custom user attributes. 6 | 7 | The custom sign-in flow is implemented in a single component to demonstrate how to use ClerkJS with minimum effort. On the contrary, the custom sign-up flow has been fully componentized and leverages ClerkJS frontend state management. We also used [React-hook-form](https://react-hook-form.com) to build our forms. 8 | 9 | Additionally, the project demonstrates how to synchronize data between your back-end (server) and Clerk.dev. The recommended method for syncing user data is to set up a server endpoint which receives [Clerk webhooks](https://clerk.dev/docs/integration/webhooks) for user related events. 10 | 11 | [![Open in VS Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/clerkinc/clerk-nextjs-examples) 12 | 13 | ## Live demo 14 | 15 | https://nextjs.widget.clerk.app/ 16 | 17 | ## Getting Started 18 | 19 | ### 1. Setup environment variables 20 | 21 | First, you need to set two environment variables for Clerk's SDK. Go to the API Keys page and start by copying the Frontend API key. Then, add it a .env.local file in your application root, with the name `NEXT_PUBLIC_CLERK_FRONTEND_API`: 22 | 23 | ```bash 24 | # Replace [frontend-api-key] with your actual Frontend API key 25 | echo "NEXT_PUBLIC_CLERK_FRONTEND_API=[frontend-api-key]" >> .env.local 26 | ``` 27 | 28 | Next, go back to the API Keys page and copy the Backend API key. Then, add it in the same .env.local file, with the name `CLERK_API_KEY`: 29 | 30 | ```bash 31 | # Replace [backend-api-key] with your actual Backend API key 32 | echo "CLERK_API_KEY=[backend-api-key]" >> .env.local 33 | 34 | ``` 35 | 36 | ### 2. Run the development server 37 | 38 | ```bash 39 | npm run dev 40 | ``` 41 | 42 | Lastly, open [http://localhost:3000](http://localhost:3000) with your browser. 43 | 44 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 45 | 46 | ## Learn More 47 | 48 | To learn more about Next.js, take a look at the following resources: 49 | 50 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 51 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 52 | 53 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 54 | 55 | ## Deploy on Vercel 56 | 57 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 58 | 59 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 60 | -------------------------------------------------------------------------------- /examples/widget/assets/svg/ArrowLeft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/widget/assets/svg/Checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/widget/assets/svg/Exclamation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/widget/assets/svg/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/widget/assets/svg/Mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/widget/components/Dashboard.module.css: -------------------------------------------------------------------------------- 1 | .fullWidth { 2 | width: 100%; 3 | } 4 | 5 | .logoRow { 6 | padding: 28px 64px 36px; 7 | display: flex; 8 | justify-content: space-between; 9 | color: var(--color-primary); 10 | } 11 | 12 | .logoRow svg { 13 | fill: var(--color-primary); 14 | } 15 | 16 | .nav { 17 | border-bottom: 1px solid var(--color-primary); 18 | padding: 0 36px; 19 | } 20 | 21 | .navButton { 22 | border: none; 23 | background: none; 24 | color: var(--color-primary); 25 | padding: 16px 0px; 26 | margin: 0px 32px; 27 | cursor: pointer; 28 | border-bottom: 1px solid var(--color-primary); 29 | font-size: 16px; 30 | font-family: "Inter", sans-serif; 31 | font-weight: 500; 32 | } 33 | 34 | .main { 35 | padding: 46px 64px; 36 | } 37 | 38 | .heading { 39 | font-size: 24px; 40 | font-weight: 500; 41 | color: var(--color-primary); 42 | } 43 | 44 | .subHeading { 45 | font-size: 16px; 46 | font-weight: 400; 47 | color: var(--color-primary); 48 | display: block; 49 | margin-bottom: 48px; 50 | } 51 | 52 | .grid { 53 | display: flex; 54 | gap: 32px; 55 | } 56 | 57 | .box { 58 | background: var(--color-primary); 59 | box-shadow: 0px 12px 24px var(--color-primary); 60 | border-radius: 16px; 61 | height: 372px; 62 | width: 416px; 63 | } 64 | -------------------------------------------------------------------------------- /examples/widget/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import {UserButton} from "@clerk/nextjs"; 2 | import Logo from "../assets/svg/Logo.svg"; 3 | 4 | import styles from "./Dashboard.module.css"; 5 | 6 | export function Dashboard() { 7 | return ( 8 |
9 |
10 |
11 | 12 | 13 |
14 | 17 |
18 |
19 | Home 20 | Welcome to your application 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /examples/widget/components/GithubLink.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: "Inter", sans-serif; 3 | font-size: 0.75rem; 4 | font-weight: 400; 5 | line-height: 1rem; 6 | letter-spacing: 0em; 7 | text-align: left; 8 | 9 | display: flex; 10 | 11 | box-shadow: 0px 24px 48px 0px #00000029; 12 | border-radius: 6px; 13 | 14 | height: 40px; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 0 1.5rem; 18 | background-color: white; 19 | } 20 | 21 | .logo { 22 | position: relative; 23 | top: 2px; 24 | min-width: 56px; 25 | } 26 | 27 | .label { 28 | font-family: "Inter", sans-serif; 29 | font-size: 0.75rem; 30 | font-weight: 400; 31 | line-height: 1rem; 32 | letter-spacing: 0em; 33 | text-align: left; 34 | margin: 0 1.5rem; 35 | } 36 | 37 | .rightLink { 38 | color: #335bf1; 39 | min-width: 100px; 40 | } 41 | 42 | .rightLink a:link { 43 | text-decoration: none; 44 | color: #335bf1; 45 | } 46 | 47 | .rightLink a:visited { 48 | text-decoration: none; 49 | color: #335bf1; 50 | } 51 | 52 | .rightLink a:hover { 53 | text-decoration: underline; 54 | color: #335bf1; 55 | } 56 | 57 | .rightLink a:active { 58 | text-decoration: underline; 59 | color: #335bf1; 60 | } 61 | 62 | .rightArrow { 63 | position: relative; 64 | top: 2px; 65 | left: 3px; 66 | } 67 | -------------------------------------------------------------------------------- /examples/widget/components/GithubLink.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./GithubLink.module.css"; 2 | import Image from "next/image"; 3 | 4 | type GithubLinkProps = { 5 | label: string; 6 | repoLink: string; 7 | }; 8 | const GithubLink = (props: GithubLinkProps) => ( 9 | <> 10 |
11 | 12 | clerk 13 | 14 |
{props.label}
15 | 23 |
24 | 25 | ); 26 | 27 | export default GithubLink; 28 | -------------------------------------------------------------------------------- /examples/widget/components/Promotion.module.css: -------------------------------------------------------------------------------- 1 | .promotion > div { 2 | margin-bottom: 32px; 3 | color: var(--color-white); 4 | } 5 | 6 | .video { 7 | width: 360px; 8 | height: 200px; 9 | background: black; 10 | border-radius: 6px; 11 | } 12 | 13 | .video iframe { 14 | max-width: 360px; 15 | max-height: 200px; 16 | border-radius: 6px; 17 | } 18 | 19 | .benefitsListItem { 20 | display: flex; 21 | align-items: center; 22 | margin-bottom: 1rem; 23 | font-weight: 500; 24 | } 25 | 26 | .benefitsListItem svg { 27 | align-self: flex-start; 28 | margin-top: 2px; 29 | } 30 | 31 | .benefitsContent { 32 | margin-left: 18px; 33 | } 34 | 35 | .benefitsTitle { 36 | font-weight: 500; 37 | font-size: 16px; 38 | line-height: 24px; 39 | } 40 | 41 | .benefitsSubtitle { 42 | font-weight: 400; 43 | font-size: 14px; 44 | line-height: 20px; 45 | color: var(--color-white-lighter); 46 | } 47 | -------------------------------------------------------------------------------- /examples/widget/components/Promotion.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Promotion.module.css"; 2 | import Logo from "../assets/svg/Logo.svg"; 3 | import Checkmark from "../assets/svg/Checkmark.svg"; 4 | import YouTube from "react-youtube"; 5 | 6 | export function Promotion(): JSX.Element { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
    17 |
  • 18 | 19 |
    20 |

    21 | Fastest widget integration in the west 22 |

    23 |

    24 | And the east, for that matter. 25 |

    26 |
    27 |
  • 28 |
  • 29 | 30 |
    31 |

    32 | Widgets so good they’ll knock your socks off 33 |

    34 |

    35 | Hope you like going barefoot! 36 |

    37 |
    38 |
  • 39 |
  • 40 | 41 |
    42 |

    43 | Widgets on widgets on widgets 44 |

    45 |

    46 | What are we going to do with all these
    widgets?? 47 |

    48 |
    49 |
  • 50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/SignInCode.module.css: -------------------------------------------------------------------------------- 1 | .emailNotice { 2 | color: var(--color-black-lighter); 3 | } 4 | 5 | .emailAddress { 6 | color: var(--color-black); 7 | } 8 | 9 | .actionButton button { 10 | width: 100%; 11 | } 12 | 13 | .resendCode { 14 | display: block; 15 | color: var(--color-primary); 16 | font-size: 12px; 17 | margin-top: 8px; 18 | background: none; 19 | border: none; 20 | font-weight: 500; 21 | } 22 | 23 | .resendCode:focus, 24 | .resendCode:hover { 25 | text-decoration: underline; 26 | } 27 | 28 | .resendCode:disabled { 29 | opacity: 0.5; 30 | } 31 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/SignInCode.tsx: -------------------------------------------------------------------------------- 1 | import {useSignIn} from "@clerk/clerk-react"; 2 | import React from "react"; 3 | import {SubmitHandler, useForm} from "react-hook-form"; 4 | import {Button} from "../common/Button"; 5 | import {Input} from "../common/Input"; 6 | import {VerifyCodeNotice} from "../common/VerifyCodeNotice"; 7 | import {APIResponseError, parseError} from "../utils/errors"; 8 | import {Validations} from "../utils/formValidations"; 9 | 10 | import styles from "./SignInCode.module.css"; 11 | 12 | type SignInCodeProps = { 13 | emailAddress: string; 14 | onDone: (sessionId: string) => void; 15 | }; 16 | 17 | export function SignInCode({ 18 | emailAddress, 19 | onDone, 20 | }: SignInCodeProps) { 21 | const { isLoaded, signIn } = useSignIn(); 22 | const [isLoading, setIsLoading] = React.useState(false); 23 | 24 | const { 25 | register, 26 | handleSubmit, 27 | formState: { errors }, 28 | setError, 29 | } = useForm<{ code: string }>(); 30 | 31 | if(!isLoaded) { 32 | return null; 33 | } 34 | 35 | const verifySignInCode: SubmitHandler<{ code: string }> = async function ({ 36 | code, 37 | }) { 38 | try { 39 | setIsLoading(true); 40 | const signInAttempt = await signIn.attemptFirstFactor({ 41 | strategy: "email_code", 42 | code, 43 | }); 44 | if (signInAttempt.status === "complete") { 45 | onDone(signInAttempt.createdSessionId!); 46 | } 47 | } catch (err) { 48 | setError("code", { 49 | type: "manual", 50 | message: parseError(err as APIResponseError), 51 | }); 52 | } finally { 53 | setIsLoading(false); 54 | } 55 | }; 56 | 57 | const resendSignInCode = async function () { 58 | const emailCodeFactor = signIn.supportedFirstFactors.find( 59 | (factor) => factor.strategy === "email_code" 60 | ); 61 | 62 | await signIn.prepareFirstFactor({ 63 | strategy: "email_code", 64 | // @ts-ignore 65 | email_address_id: emailCodeFactor.email_address_id, 66 | }); 67 | }; 68 | 69 | return ( 70 |
71 | 75 | 81 |
82 | 83 |
84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/SignInForm.module.css: -------------------------------------------------------------------------------- 1 | .formLayout { 2 | margin: 0 auto; 3 | max-width: 360px; 4 | } 5 | 6 | .formLayout > div, 7 | .formLayout > form > div { 8 | width: 100%; 9 | margin-bottom: 32px; 10 | } 11 | 12 | .actionButton button { 13 | width: 100%; 14 | } 15 | 16 | .backLink { 17 | background: none; 18 | color: var(--color-primary); 19 | } 20 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/SignInForm.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Logo from "../../assets/svg/Logo.svg"; 3 | import ArrowLeft from "../../assets/svg/ArrowLeft.svg"; 4 | 5 | import styles from "./SignInForm.module.css"; 6 | import {Notice} from "../common/Notice"; 7 | import {Title} from "../common/Title"; 8 | import {Input} from "../common/Input"; 9 | import {useSignIn} from "@clerk/nextjs"; 10 | import {useRouter} from "next/router"; 11 | import {useForm} from "react-hook-form"; 12 | import {Button} from "../common/Button"; 13 | import {APIResponseError, parseError} from "../utils/errors"; 14 | import {SignInCode} from "./SignInCode"; 15 | import {Validations} from "../utils/formValidations"; 16 | import {SignInPassword} from "./SignInPassword"; 17 | import {VerificationSwitcher} from "./VerificationSwitcher"; 18 | import {EmailCodeFactor} from "@clerk/types"; 19 | 20 | interface SignInInputs { 21 | emailAddress: string; 22 | } 23 | 24 | export enum SignInFormSteps { 25 | EMAIL, 26 | CODE, 27 | PASSWORD, 28 | } 29 | 30 | export function SignInForm() { 31 | const {isLoaded, signIn, setSession} = useSignIn(); 32 | const router = useRouter(); 33 | const [firstName, setFirstName] = React.useState(""); 34 | const [isLoading, setIsLoading] = React.useState(false); 35 | 36 | const [formStep, setFormStep] = React.useState(SignInFormSteps.EMAIL); 37 | const { 38 | register, 39 | handleSubmit, 40 | setError, 41 | getValues, 42 | formState: { errors }, 43 | } = useForm(); 44 | 45 | if(!isLoaded) { 46 | return null; 47 | } 48 | 49 | const sendSignInCode = async function () { 50 | const emailAddress = getValues("emailAddress"); 51 | const signInAttempt = await signIn.create({ 52 | identifier: emailAddress, 53 | }); 54 | 55 | const emailCodeFactor = signInAttempt.supportedFirstFactors.find( 56 | (factor) => factor.strategy === "email_code" 57 | ) as EmailCodeFactor; 58 | 59 | setFirstName(signInAttempt.userData.firstName || ""); 60 | await signInAttempt.prepareFirstFactor({ 61 | strategy: "email_code", 62 | emailAddressId: emailCodeFactor.emailAddressId, 63 | }); 64 | }; 65 | 66 | const verifyEmail = async function () { 67 | try { 68 | setIsLoading(true); 69 | await sendSignInCode(); 70 | setFormStep(SignInFormSteps.CODE); 71 | } catch (err) { 72 | setError("emailAddress", { 73 | type: "manual", 74 | message: parseError(err as APIResponseError), 75 | }); 76 | } finally { 77 | setIsLoading(false); 78 | } 79 | }; 80 | 81 | const signUpComplete = async (createdSessionId: string) => { 82 | /** Couldn't the signin be updated and have the createdSessionId ? */ 83 | setSession(createdSessionId, () => router.push("/dashboard")); 84 | }; 85 | 86 | return ( 87 |
88 | {formStep !== SignInFormSteps.EMAIL && ( 89 | setFormStep(SignInFormSteps.EMAIL)} /> 90 | )} 91 |
92 | 93 |
94 | 98 | {formStep === SignInFormSteps.EMAIL && ( 99 | <form onSubmit={handleSubmit(verifyEmail)}> 100 | <Input 101 | label="Email" 102 | {...register("emailAddress", Validations.emailAddress)} 103 | errorText={errors.emailAddress?.message} 104 | autoFocus 105 | /> 106 | <div className={styles.actionButton}> 107 | <Button isLoading={isLoading}>Continue</Button> 108 | </div> 109 | <Notice 110 | content="Don’t have an account?" 111 | actionLink="/sign-up" 112 | actionMessage="Sign up" 113 | /> 114 | </form> 115 | )} 116 | {formStep === SignInFormSteps.CODE && ( 117 | <SignInCode 118 | onDone={signUpComplete} 119 | emailAddress={getValues("emailAddress")} 120 | /> 121 | )} 122 | {formStep === SignInFormSteps.PASSWORD && ( 123 | <SignInPassword onDone={signUpComplete} /> 124 | )} 125 | <VerificationSwitcher 126 | formStep={formStep} 127 | onSwitchVerificationMethod={setFormStep} 128 | /> 129 | </div> 130 | ); 131 | } 132 | 133 | type BackButtonProps = { onClick: () => void }; 134 | function BackButton({ onClick }: BackButtonProps): JSX.Element { 135 | return ( 136 | <div> 137 | <button className={styles.backLink} onClick={onClick}> 138 | <ArrowLeft /> Back 139 | </button> 140 | </div> 141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/SignInPassword.module.css: -------------------------------------------------------------------------------- 1 | .actionButton button { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/SignInPassword.tsx: -------------------------------------------------------------------------------- 1 | import {useSignIn} from "@clerk/nextjs"; 2 | import React from "react"; 3 | import {SubmitHandler, useForm} from "react-hook-form"; 4 | import {Button} from "../common/Button"; 5 | import {Input} from "../common/Input"; 6 | import {APIResponseError, parseError} from "../utils/errors"; 7 | import {Validations} from "../utils/formValidations"; 8 | 9 | import styles from "./SignInPassword.module.css"; 10 | 11 | type SignInPasswordProps = { 12 | onDone: (sessionId: string) => void; 13 | }; 14 | 15 | export function SignInPassword({ onDone }: SignInPasswordProps) { 16 | const {isLoaded, signIn} = useSignIn(); 17 | const [isLoading, setIsLoading] = React.useState(false); 18 | 19 | const { 20 | register, 21 | handleSubmit, 22 | formState: { errors }, 23 | setError, 24 | } = useForm<{ password: string }>(); 25 | 26 | if(!isLoaded) { 27 | return null; 28 | } 29 | 30 | const verifyPassword: SubmitHandler<{ password: string }> = async function ({ 31 | password, 32 | }) { 33 | try { 34 | setIsLoading(true); 35 | const signInAttempt = await signIn.attemptFirstFactor({ 36 | strategy: "password", 37 | password, 38 | }); 39 | if (signInAttempt.status === "complete") { 40 | onDone(signInAttempt.createdSessionId!); 41 | } 42 | } catch (err) { 43 | setError("password", { 44 | type: "manual", 45 | message: parseError(err as APIResponseError), 46 | }); 47 | } finally { 48 | setIsLoading(false); 49 | } 50 | }; 51 | 52 | return ( 53 | <form onSubmit={handleSubmit(verifyPassword)}> 54 | <Input 55 | label="Password" 56 | type="password" 57 | {...register("password", Validations.password)} 58 | errorText={errors.password?.message} 59 | /> 60 | <div className={styles.actionButton}> 61 | <Button isLoading={isLoading}>Continue</Button> 62 | </div> 63 | </form> 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/VerificationSwitcher.module.css: -------------------------------------------------------------------------------- 1 | .switchVerificationMethod { 2 | display: block; 3 | color: var(--color-primary); 4 | font-size: 12px; 5 | margin-top: 8px; 6 | background: none; 7 | border: none; 8 | font-weight: 500; 9 | margin: 0 auto; 10 | } 11 | 12 | .switchVerificationMethod:focus, 13 | .switchVerificationMethod:hover { 14 | text-decoration: underline; 15 | } 16 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/VerificationSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./VerificationSwitcher.module.css"; 2 | import { SignInFormSteps } from "./SignInForm"; 3 | 4 | type VerificationSteps = SignInFormSteps.CODE | SignInFormSteps.PASSWORD; 5 | 6 | type VerificationSwitcherProps = { 7 | formStep: SignInFormSteps; 8 | onSwitchVerificationMethod: (step: VerificationSteps) => void; 9 | }; 10 | 11 | export function VerificationSwitcher({ 12 | formStep, 13 | onSwitchVerificationMethod, 14 | }: VerificationSwitcherProps): JSX.Element | null { 15 | const alternateFormStep = 16 | formStep === SignInFormSteps.CODE 17 | ? SignInFormSteps.PASSWORD 18 | : SignInFormSteps.CODE; 19 | 20 | if (formStep === SignInFormSteps.EMAIL) { 21 | return null; 22 | } 23 | 24 | return ( 25 | <div> 26 | <button 27 | onClick={() => onSwitchVerificationMethod(alternateFormStep)} 28 | className={styles.switchVerificationMethod} 29 | > 30 | Try another method 31 | </button> 32 | </div> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/widget/components/SignInForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SignInForm"; 2 | -------------------------------------------------------------------------------- /examples/widget/components/SignUpForm/SignUpCode.module.css: -------------------------------------------------------------------------------- 1 | .actionButton { 2 | display: flex; 3 | justify-content: flex-end; 4 | } 5 | 6 | .actionButton button { 7 | display: flex; 8 | justify-content: center; 9 | width: 130px; 10 | } 11 | -------------------------------------------------------------------------------- /examples/widget/components/SignUpForm/SignUpCode.tsx: -------------------------------------------------------------------------------- 1 | import { useSignUp } from "@clerk/nextjs"; 2 | import React from "react"; 3 | import { SubmitHandler, useForm } from "react-hook-form"; 4 | import { Button } from "../common/Button"; 5 | import { Input } from "../common/Input"; 6 | import { VerifyCodeNotice } from "../common/VerifyCodeNotice"; 7 | 8 | import { APIResponseError, parseError } from "../utils/errors"; 9 | import { Validations } from "../utils/formValidations"; 10 | 11 | import styles from "./SignUpCode.module.css"; 12 | 13 | export function SignUpCode({ 14 | emailAddress, 15 | onDone, 16 | }: { 17 | emailAddress: string; 18 | onDone: (sessionId: string) => void; 19 | }) { 20 | const { isLoaded, signUp } = useSignUp(); 21 | const [isLoading, setIsLoading] = React.useState(false); 22 | 23 | const { 24 | register, 25 | handleSubmit, 26 | formState: { errors }, 27 | setError, 28 | } = useForm<{ code: string }>(); 29 | 30 | if (!isLoaded) { 31 | return null; 32 | } 33 | 34 | const verifySignUpCode: SubmitHandler<{ code: string }> = async function ({ 35 | code, 36 | }) { 37 | try { 38 | setIsLoading(true); 39 | const signUpAttempt = await signUp.attemptEmailAddressVerification({ 40 | code, 41 | }); 42 | 43 | if (signUpAttempt.status === "complete") { 44 | onDone(signUpAttempt.createdSessionId!); 45 | } 46 | } catch (err) { 47 | setError("code", { 48 | type: "manual", 49 | message: parseError(err as APIResponseError), 50 | }); 51 | } finally { 52 | setIsLoading(false); 53 | } 54 | }; 55 | 56 | const resendSignUpCode = async function () { 57 | await signUp.prepareEmailAddressVerification(); 58 | }; 59 | 60 | return ( 61 | <form onSubmit={handleSubmit(verifySignUpCode)}> 62 | <VerifyCodeNotice 63 | onResendClick={resendSignUpCode} 64 | emailAddress={emailAddress} 65 | /> 66 | <Input 67 | label="Code" 68 | {...register("code", Validations.oneTimeCode)} 69 | errorText={errors.code?.message} 70 | autoFocus 71 | /> 72 | <div className={styles.actionButton}> 73 | <Button isLoading={isLoading}>Verify</Button> 74 | </div> 75 | </form> 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /examples/widget/components/SignUpForm/SignUpForm.module.css: -------------------------------------------------------------------------------- 1 | .form { 2 | max-width: 360px; 3 | } 4 | 5 | .form form > div { 6 | margin-bottom: 32px; 7 | width: 100%; 8 | } 9 | 10 | .actionButton { 11 | display: flex; 12 | justify-content: flex-end; 13 | } 14 | 15 | .actionButton button { 16 | display: flex; 17 | justify-content: center; 18 | width: 130px; 19 | } 20 | -------------------------------------------------------------------------------- /examples/widget/components/SignUpForm/SignUpForm.tsx: -------------------------------------------------------------------------------- 1 | import {useSignUp} from "@clerk/clerk-react"; 2 | import {useRouter} from "next/router"; 3 | import React from "react"; 4 | import {SubmitHandler, useForm} from "react-hook-form"; 5 | import USA from "../../assets/svg/USA.svg"; 6 | import {Button} from "../common/Button"; 7 | import {ErrorMessage} from "../common/ErrorMessage"; 8 | import {Input} from "../common/Input"; 9 | import {Title} from "../common/Title"; 10 | import {APIResponseError, parseError} from "../utils/errors"; 11 | import {Notice} from "../common/Notice"; 12 | import {SignUpCode} from "./SignUpCode"; 13 | import styles from "./SignUpForm.module.css"; 14 | import {Terms} from "./Terms"; 15 | import {Validations} from "../utils/formValidations"; 16 | 17 | interface SignUpInputs { 18 | name: string; 19 | company: string; 20 | emailAddress: string; 21 | country: string; 22 | phone: string; 23 | password: string; 24 | clerkError?: string; 25 | } 26 | 27 | enum SignUpFormSteps { 28 | FORM, 29 | CODE, 30 | } 31 | 32 | export function SignUpForm() { 33 | const {isLoaded, setSession, signUp} = useSignUp(); 34 | const router = useRouter(); 35 | const [isLoading, setIsLoading] = React.useState(false); 36 | 37 | const [formStep, setFormStep] = React.useState(SignUpFormSteps.FORM); 38 | const { 39 | register, 40 | handleSubmit, 41 | getValues, 42 | setError, 43 | formState: { errors }, 44 | watch, 45 | clearErrors, 46 | } = useForm<SignUpInputs>({ defaultValues: { country: "USA" } }); 47 | 48 | if(!isLoaded) { 49 | return null; 50 | } 51 | 52 | const onSubmit: SubmitHandler<SignUpInputs> = async ({ 53 | emailAddress, 54 | password, 55 | name, 56 | phone, 57 | country, 58 | company, 59 | }) => { 60 | try { 61 | setIsLoading(true); 62 | const [firstName, lastName] = name.split(/\s+/); 63 | const signUpAttempt = await signUp.create({ 64 | emailAddress, 65 | password, 66 | lastName, 67 | firstName, 68 | unsafeMetadata: { 69 | country, 70 | company, 71 | phone, 72 | }, 73 | }); 74 | await signUpAttempt.prepareEmailAddressVerification(); 75 | setFormStep(SignUpFormSteps.CODE); 76 | } catch (err) { 77 | setError("clerkError", { 78 | type: "manual", 79 | message: parseError(err as APIResponseError), 80 | }); 81 | } finally { 82 | setIsLoading(false); 83 | } 84 | }; 85 | 86 | /** Clerk API related errors on change. */ 87 | watch(() => errors.clerkError && clearErrors("clerkError")); 88 | 89 | const signUpComplete = async (createdSessionId: string) => { 90 | /** Couldn't the signup be updated and have the createdSessionId ? */ 91 | await setSession(createdSessionId, () => router.push("/dashboard")); 92 | }; 93 | 94 | return ( 95 | <div className={styles.form}> 96 | <Title 97 | content={"Sign up"} 98 | subtitle={ 99 | "Create an account and start integrating widgets in minutes, not days" 100 | } 101 | /> 102 | {formStep === SignUpFormSteps.FORM && ( 103 | <form onSubmit={handleSubmit(onSubmit)} noValidate> 104 | <Notice 105 | content="Already have an account?" 106 | actionLink="/sign-in" 107 | actionMessage="Sign in instead" 108 | /> 109 | <div> 110 | <Input 111 | {...register("name", Validations.name)} 112 | label="Full name" 113 | errorText={errors.name?.message} 114 | /> 115 | <Input 116 | label="Company" 117 | {...register("company", Validations.company)} 118 | errorText={errors.company?.message} 119 | /> 120 | <Input 121 | label="Email" 122 | type="email" 123 | {...register("emailAddress", Validations.emailAddress)} 124 | errorText={errors.emailAddress?.message} 125 | /> 126 | <Input 127 | label="Country" 128 | badge={<USA />} 129 | {...register("country", Validations.country)} 130 | errorText={errors.country?.message} 131 | disabled 132 | /> 133 | <Input 134 | label="Phone" 135 | {...register("phone", Validations.phone)} 136 | badge={"+1"} 137 | errorText={errors.phone?.message} 138 | /> 139 | <Input 140 | {...register("password", Validations.password)} 141 | label="Create password" 142 | type="password" 143 | errorText={errors.password?.message} 144 | /> 145 | </div> 146 | <Terms /> 147 | {errors.clerkError?.message && ( 148 | <div> 149 | <ErrorMessage message={errors.clerkError.message} /> 150 | </div> 151 | )} 152 | <div className={styles.actionButton}> 153 | <Button isLoading={isLoading}>Create account</Button> 154 | </div> 155 | </form> 156 | )} 157 | {formStep === SignUpFormSteps.CODE && ( 158 | <SignUpCode 159 | emailAddress={getValues("emailAddress")} 160 | onDone={signUpComplete} 161 | /> 162 | )} 163 | </div> 164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /examples/widget/components/SignUpForm/Terms.module.css: -------------------------------------------------------------------------------- 1 | .terms { 2 | color: var(--color-black-lighter); 3 | font-size: 12px; 4 | line-height: 16px; 5 | } 6 | 7 | .terms > span > span { 8 | text-decoration: underline; 9 | } 10 | -------------------------------------------------------------------------------- /examples/widget/components/SignUpForm/Terms.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Terms.module.css"; 2 | 3 | export function Terms(): JSX.Element { 4 | return ( 5 | <div className={styles.terms}> 6 | <span> 7 | By creating an account, you agree to Widget’s{" "} 8 | <span>Terms of Service</span> and <span>Privacy Policy</span> 9 | </span> 10 | </div> 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/widget/components/SignUpForm/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./SignUpForm"; 2 | -------------------------------------------------------------------------------- /examples/widget/components/common/Button.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background: var(--color-primary); 3 | color: var(--color-white); 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | padding: 10px 20px; 8 | border-radius: 4px; 9 | font-weight: 600; 10 | font-size: 12px; 11 | } 12 | 13 | .button:focus-within, 14 | .button:focus { 15 | filter: drop-shadow(0px 3px 3px var(--color-primary)); 16 | } 17 | -------------------------------------------------------------------------------- /examples/widget/components/common/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Button.module.css"; 3 | import { Spinner } from "./Spinner"; 4 | 5 | const Button = React.forwardRef< 6 | HTMLButtonElement, 7 | { 8 | children: React.ReactNode; 9 | isLoading?: boolean; 10 | } & React.ButtonHTMLAttributes<HTMLButtonElement> 11 | >(({ children, isLoading = false, ...rest }, ref) => { 12 | return ( 13 | <button 14 | type="submit" 15 | {...rest} 16 | className={styles.button} 17 | ref={ref} 18 | disabled={isLoading} 19 | > 20 | {isLoading ? <Spinner /> : children} 21 | </button> 22 | ); 23 | }); 24 | 25 | Button.displayName = "Button"; 26 | 27 | export { Button }; 28 | -------------------------------------------------------------------------------- /examples/widget/components/common/ErrorMessage.module.css: -------------------------------------------------------------------------------- 1 | .errorMessage { 2 | display: block; 3 | font-size: 12px; 4 | color: var(--color-error); 5 | margin-top: 8px; 6 | } 7 | -------------------------------------------------------------------------------- /examples/widget/components/common/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { CSSTransition } from "react-transition-group"; 3 | import styles from "./ErrorMessage.module.css"; 4 | 5 | export function ErrorMessage({ message }: { message: string }): JSX.Element { 6 | return ( 7 | <CSSTransition 8 | in={Boolean(message)} 9 | timeout={200} 10 | classNames="errorDisplay" 11 | > 12 | <span className={clsx(styles.errorMessage, "errorDisplay")}> 13 | {message} 14 | </span> 15 | </CSSTransition> 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/widget/components/common/Input.module.css: -------------------------------------------------------------------------------- 1 | .label { 2 | font-size: 14px; 3 | font-weight: 500; 4 | } 5 | 6 | .input { 7 | display: block; 8 | width: 100%; 9 | font-size: 12px; 10 | font-weight: 400; 11 | margin-top: 4px; 12 | border-radius: 6px; 13 | padding: 10px 16px; 14 | border: 1px solid rgba(0, 0, 0, 0.16); 15 | } 16 | 17 | .inputWithError { 18 | border: 1px solid var(--color-error); 19 | } 20 | 21 | .input[type="password"] { 22 | letter-spacing: 3px; 23 | } 24 | 25 | .inputContainer { 26 | position: relative; 27 | } 28 | 29 | .inputField { 30 | margin-bottom: 16px; 31 | } 32 | 33 | .inputField :global .errorDisplay { 34 | opacity: 0; 35 | transform: scale(1); 36 | } 37 | 38 | .inputField :global .errorDisplay-enter-done { 39 | opacity: 1; 40 | transform: translateX(0); 41 | transition: opacity 200ms, transform 200ms; 42 | } 43 | 44 | .inputWithBadge { 45 | padding-left: 52px; 46 | } 47 | 48 | .badge { 49 | border-radius: 6px 0px 0px 6px; 50 | width: 40px; 51 | position: absolute; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | height: 100%; 56 | background: rgba(0, 0, 0, 0.04); 57 | color: rgba(0, 0, 0, 0.5); 58 | } 59 | -------------------------------------------------------------------------------- /examples/widget/components/common/Input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React from "react"; 3 | import { ErrorMessage } from "./ErrorMessage"; 4 | import styles from "./Input.module.css"; 5 | 6 | const Input = React.forwardRef< 7 | HTMLInputElement, 8 | { 9 | label: string; 10 | errorText?: string; 11 | onPaste?: React.ClipboardEventHandler<HTMLInputElement>; 12 | autoFocus?: boolean; 13 | type?: string; 14 | badge?: React.ReactNode | string; 15 | } & React.InputHTMLAttributes<HTMLInputElement> 16 | >( 17 | ( 18 | { 19 | autoFocus = true, 20 | type = "text", 21 | badge, 22 | label, 23 | errorText, 24 | onPaste, 25 | ...rest 26 | }, 27 | ref 28 | ) => { 29 | return ( 30 | <> 31 | {label && ( 32 | <label className={styles.label}> 33 | <div className={styles.inputField}> 34 | {label} 35 | <div className={styles.inputContainer}> 36 | {badge && <div className={styles.badge}>{badge}</div>} 37 | <input 38 | autoFocus={autoFocus} 39 | onPaste={onPaste} 40 | className={clsx( 41 | styles.input, 42 | badge && styles.inputWithBadge, 43 | errorText && styles.inputWithError 44 | )} 45 | ref={ref} 46 | type={type} 47 | {...rest} 48 | /> 49 | </div> 50 | {<ErrorMessage message={errorText || ""} />} 51 | </div> 52 | </label> 53 | )} 54 | </> 55 | ); 56 | } 57 | ); 58 | 59 | Input.displayName = "Input"; 60 | 61 | export { Input }; 62 | -------------------------------------------------------------------------------- /examples/widget/components/common/Notice.module.css: -------------------------------------------------------------------------------- 1 | .notice { 2 | display: flex; 3 | background: rgba(0, 0, 0, 0.04); 4 | border-radius: 3px; 5 | padding: 14px 18px; 6 | line-height: 16px; 7 | } 8 | 9 | .notice span { 10 | font-size: 12px; 11 | margin-left: 6px; 12 | color: rgba(0, 0, 0, 0.8); 13 | } 14 | 15 | .notice a { 16 | color: var(--color-primary); 17 | font-size: 12px; 18 | text-decoration: underline; 19 | margin-left: 4px; 20 | } 21 | -------------------------------------------------------------------------------- /examples/widget/components/common/Notice.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Notice.module.css"; 2 | import Link from "next/link"; 3 | 4 | import Exclamation from "../../assets/svg/Exclamation.svg"; 5 | 6 | export function Notice({ 7 | content, 8 | actionLink, 9 | actionMessage, 10 | }: { 11 | content: string; 12 | actionLink: string; 13 | actionMessage: string; 14 | }): JSX.Element { 15 | return ( 16 | <div className={styles.notice}> 17 | <Exclamation /> 18 | <span>{content}</span> 19 | <Link href={actionLink}> 20 | <a>{actionMessage}</a> 21 | </Link> 22 | </div> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /examples/widget/components/common/Spinner.module.css: -------------------------------------------------------------------------------- 1 | @keyframes spinning { 2 | 0% { 3 | transform: rotate(0deg); 4 | } 5 | 100% { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | 10 | .spinner { 11 | display: inline-block; 12 | border: 2px solid var(--color-white); 13 | border-left: 2px solid var(--color-gray); 14 | animation: spinning 1s infinite linear; 15 | border-radius: 50%; 16 | width: 1rem; 17 | height: 1rem; 18 | } 19 | -------------------------------------------------------------------------------- /examples/widget/components/common/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Spinner.module.css"; 2 | 3 | export function Spinner() { 4 | return <div className={styles.spinner}></div>; 5 | } 6 | -------------------------------------------------------------------------------- /examples/widget/components/common/Title.module.css: -------------------------------------------------------------------------------- 1 | .titleContainer { 2 | margin-bottom: 32px; 3 | } 4 | 5 | .title { 6 | font-weight: 600; 7 | font-size: 20px; 8 | line-height: 28px; 9 | margin-bottom: 2px; 10 | } 11 | 12 | .subtitle { 13 | font-weight: 400; 14 | font-size: 14px; 15 | line-height: 20px; 16 | color: var(--color-black-lighter); 17 | } 18 | -------------------------------------------------------------------------------- /examples/widget/components/common/Title.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Title.module.css"; 2 | 3 | export function Title({ 4 | content, 5 | subtitle, 6 | }: { 7 | content: string; 8 | subtitle: string; 9 | }): JSX.Element { 10 | return ( 11 | <div className={styles.titleContainer}> 12 | <p className={styles.title}>{content}</p> 13 | <p className={styles.subtitle}>{subtitle}</p> 14 | </div> 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/widget/components/common/VerifyCodeNotice.module.css: -------------------------------------------------------------------------------- 1 | .emailNotice { 2 | color: var(--color-black-lighter); 3 | } 4 | 5 | .emailAddress { 6 | color: var(--color-black); 7 | } 8 | 9 | .resendCode { 10 | display: block; 11 | color: var(--color-primary); 12 | font-size: 12px; 13 | margin-top: 8px; 14 | background: none; 15 | border: none; 16 | font-weight: 500; 17 | } 18 | 19 | .resendCode:focus, 20 | .resendCode:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | .resendCode:disabled { 25 | opacity: 0.5; 26 | } 27 | -------------------------------------------------------------------------------- /examples/widget/components/common/VerifyCodeNotice.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./VerifyCodeNotice.module.css"; 3 | 4 | export function VerifyCodeNotice({ 5 | emailAddress, 6 | onResendClick, 7 | }: { 8 | emailAddress: string; 9 | onResendClick: () => void; 10 | }): JSX.Element { 11 | const [resendCodeDisabled, setResendCodeDisabled] = React.useState(false); 12 | 13 | const handleResendClick = async function () { 14 | try { 15 | setResendCodeDisabled(true); 16 | await onResendClick(); 17 | } finally { 18 | setResendCodeDisabled(false); 19 | } 20 | }; 21 | 22 | return ( 23 | <div className={styles.emailNotice}> 24 | Enter the 6-digit code sent to <br /> 25 | <span className={styles.emailAddress}>{emailAddress}</span> 26 | <button 27 | type="button" 28 | disabled={resendCodeDisabled} 29 | onClick={handleResendClick} 30 | className={styles.resendCode} 31 | > 32 | Resend code 33 | </button> 34 | </div> 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /examples/widget/components/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import type { ClerkAPIError } from "@clerk/types"; 2 | 3 | export interface APIResponseError { 4 | errors: ClerkAPIError[]; 5 | } 6 | 7 | export function parseError(err: APIResponseError): string { 8 | if (!err) { 9 | return ""; 10 | } 11 | 12 | if (err.errors) { 13 | return err.errors[0].longMessage || ""; 14 | } 15 | 16 | throw err; 17 | } 18 | -------------------------------------------------------------------------------- /examples/widget/components/utils/formValidations.ts: -------------------------------------------------------------------------------- 1 | import { RegisterOptions } from "react-hook-form"; 2 | 3 | const EMAIL_REGEX_PATTERN = /^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; 4 | 5 | const FULL_NAME_PATTERN = /(?=^.{0,40}$)^[a-zA-Z-]+\s[a-zA-Z-]+$/; 6 | 7 | export const Validations: { 8 | [x: string]: RegisterOptions; 9 | } = { 10 | name: { 11 | required: { value: true, message: "Enter full name" }, 12 | minLength: { 13 | value: 4, 14 | message: "Full name cannot be less than 4 characters", 15 | }, 16 | pattern: { 17 | value: FULL_NAME_PATTERN, 18 | message: "Full name must be in the correct format e.g. John Doe ", 19 | }, 20 | }, 21 | company: { 22 | required: { value: true, message: "Enter company name" }, 23 | minLength: 2, 24 | }, 25 | emailAddress: { 26 | required: { value: true, message: "Enter email address" }, 27 | pattern: { 28 | value: EMAIL_REGEX_PATTERN, 29 | message: "Must be an email", 30 | }, 31 | }, 32 | country: { 33 | required: { value: true, message: "Enter country name" }, 34 | minLength: 3, 35 | }, 36 | phone: { 37 | required: { value: true, message: "Enter phone" }, 38 | pattern: { 39 | value: /^\d{10}$/, 40 | message: "Phone number must be 10 digits", 41 | }, 42 | }, 43 | password: { 44 | required: { value: true, message: "Enter a password" }, 45 | minLength: { 46 | value: 8, 47 | message: "Password must be at least 8 characters long", 48 | }, 49 | }, 50 | oneTimeCode: { 51 | required: { value: true, message: "Enter your one-time code" }, 52 | pattern: { 53 | value: /^\d{6}$/, 54 | message: "One-time code is a 6 digit number", 55 | }, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /examples/widget/db/README.md: -------------------------------------------------------------------------------- 1 | # Database 2 | 3 | This example uses the [Prisma](https://www.prisma.io/) ORM on top of a [PostgreSQL](https://www.postgresql.org/) database. 4 | 5 | This means there are some additional steps you need to perform in order to configure your database. 6 | 7 | ## PostgreSQL instance 8 | 9 | You need to have a working installation of PostgreSQL and a postgres server instance running. The installation steps depend on your OS, but you can find more information in the [PostgreSQL](https://www.postgresql.org/download/) installation documentation. 10 | 11 | After you have your postgres server up and running, you need to take note of the database connection URL string. Feel free to use any method you prefer to connect and modify the connection string accordingly. 12 | 13 | ``` 14 | postgresql://username:password@localhost:5432/widget_development?schema=public 15 | ``` 16 | 17 | ## Set up Prisma 18 | 19 | Follow the [Prisma documentation](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/connect-your-database-typescript-postgres) to connect your database to prisma and migrate your schema. 20 | 21 | In essence, you'll have to 22 | 23 | - Let Prisma know about your database. Create a `.env` file inside the `db/prisma` folder and set the `DATABASE_URL` environment variable with your PostgreSQL connection URL. 24 | - Generate the schema and apply the migrations. Run `yarn prisma migrate dev --name init --schema ./db/prisma/schema.prisma`. 25 | 26 | ## Schema 27 | 28 | We've created a `users` table with a very simple schema. 29 | 30 | The primary key for the `users` table is the `external_id` column. The "external" part denotes that this ID comes from Clerk. 31 | 32 | The rest of the user attributes will simply be stored in a JSON column, called `attributes`. 33 | -------------------------------------------------------------------------------- /examples/widget/db/prisma/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | // Prevent multiple instances of Prisma Client in development 4 | declare const global: typeof globalThis & { prisma?: PrismaClient }; 5 | 6 | const prisma = global.prisma || new PrismaClient(); 7 | if (process.env.NODE_ENV === "development") global.prisma = prisma; 8 | 9 | export default prisma; 10 | -------------------------------------------------------------------------------- /examples/widget/db/prisma/migrations/20211103081338_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "externalId" TEXT NOT NULL, 5 | "attributes" JSONB NOT NULL, 6 | 7 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "User_externalId_key" ON "User"("externalId"); 12 | -------------------------------------------------------------------------------- /examples/widget/db/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /examples/widget/db/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id Int @id @default(autoincrement()) 15 | externalId String @unique 16 | attributes Json 17 | } 18 | -------------------------------------------------------------------------------- /examples/widget/layouts/SignInLayout.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | width: 720px; 3 | margin: 0 auto; 4 | padding-top: 64px; 5 | background: var(--color-white); 6 | } 7 | -------------------------------------------------------------------------------- /examples/widget/layouts/SignInLayout.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./SignInLayout.module.css"; 2 | 3 | export function SignInLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }): JSX.Element { 8 | return <div className={styles.content}>{children}</div>; 9 | } 10 | -------------------------------------------------------------------------------- /examples/widget/layouts/SignUpLayout.module.css: -------------------------------------------------------------------------------- 1 | .background { 2 | min-height: 100vh; 3 | background: linear-gradient( 4 | 90deg, 5 | var(--color-primary) 50%, 6 | var(--color-white) 50% 7 | ); 8 | } 9 | 10 | .content { 11 | max-width: 1080px; 12 | padding-top: 64px; 13 | padding-bottom: 64px; 14 | margin: 0 auto; 15 | display: flex; 16 | height: 100%; 17 | flex-wrap: wrap; 18 | justify-content: space-around; 19 | } 20 | 21 | @media screen and (min-width: 1260px) { 22 | .content { 23 | justify-content: space-between; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/widget/layouts/SignUpLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Promotion } from "../components/Promotion"; 2 | import styles from "./SignUpLayout.module.css"; 3 | 4 | export function SignUpLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }): JSX.Element { 9 | return ( 10 | <div className={styles.background}> 11 | <div className={styles.content}> 12 | <Promotion /> 13 | {children} 14 | </div> 15 | </div> 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/widget/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="next" /> 2 | /// <reference types="next/image-types/global" /> 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/widget/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | webpack(config) { 5 | config.module.rules.push({ 6 | test: /\.svg$/, 7 | use: ["@svgr/webpack"], 8 | }); 9 | return config; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/widget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clerk-widget-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "schema:generate": "prisma generate", 11 | "prisma:studio": "prisma studio" 12 | }, 13 | "dependencies": { 14 | "@clerk/nextjs": "^3.7.0", 15 | "@prisma/client": "^3.3.0", 16 | "clsx": "^1.1.1", 17 | "micro": "^9.3.4", 18 | "next": "12.2.2", 19 | "react-dom": "18.2.0", 20 | "react-hook-form": "^7.17.1", 21 | "react-transition-group": "^4.4.2", 22 | "react-youtube": "^7.13.1", 23 | "react": "18.2.0", 24 | "svix": "^0.33.0" 25 | }, 26 | "devDependencies": { 27 | "@svgr/webpack": "^5.5.0", 28 | "@types/micro": "^7.3.6", 29 | "@types/react": "18.0.15", 30 | "@types/react-transition-group": "^4.4.3", 31 | "eslint": "8.0.1", 32 | "eslint-config-next": "12.2.2", 33 | "prisma": "^3.3.0", 34 | "typescript": "4.7.4" 35 | }, 36 | "prisma": { 37 | "schema": "db/prisma/schema.prisma" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/widget/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import "../styles/variables.css"; 3 | import type {AppProps} from "next/app"; 4 | import {ClerkProvider, RedirectToSignUp, SignedIn, SignedOut,} from "@clerk/nextjs"; 5 | import GithubLink from "../components/GithubLink"; 6 | import {useRouter} from "next/router"; 7 | import {Dashboard} from "../components/Dashboard"; 8 | 9 | function WidgetApp({ Component, pageProps }: AppProps) { 10 | const { pathname } = useRouter(); 11 | 12 | const publicPages = ["/sign-in/[[...index]]", "/sign-up/[[...index]]"]; 13 | 14 | return ( 15 | <> 16 | <ClerkProvider {...pageProps}> 17 | <SignedIn> 18 | <Dashboard /> 19 | </SignedIn> 20 | <SignedOut> 21 | {publicPages.includes(pathname) ? ( 22 | <Component {...pageProps} /> 23 | ) : ( 24 | <RedirectToSignUp /> 25 | )} 26 | </SignedOut> 27 | </ClerkProvider> 28 | <footer> 29 | <GithubLink 30 | label="Widget is a live demo that showcases how to add custom fields on the user" 31 | repoLink="https://github.com/clerkinc/clerk-nextjs-examples/tree/main/examples/widget" 32 | /> 33 | </footer> 34 | </> 35 | ); 36 | } 37 | export default WidgetApp; 38 | -------------------------------------------------------------------------------- /examples/widget/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | <Html> 7 | <Head> 8 | <link 9 | href="https://fonts.googleapis.com/css2?family=Inter&display=swap" 10 | rel="stylesheet" 11 | /> 12 | <link rel="icon" type="image/png" href="/favicon.png" /> 13 | </Head> 14 | <body> 15 | <Main /> 16 | <NextScript /> 17 | </body> 18 | </Html> 19 | ); 20 | } 21 | } 22 | 23 | export default MyDocument; 24 | -------------------------------------------------------------------------------- /examples/widget/pages/api/webhooks/README.md: -------------------------------------------------------------------------------- 1 | # Webhooks 2 | 3 | The project demonstrates how to set up a [Next.js API route](https://nextjs.org/docs/api-routes/introduction) to handle webhook requests coming from Clerk. 4 | 5 | In this example, the webhook endpoint listens for user related events on Clerk.dev. It handles the `user.created` and `user.updated` events, so that the app's database is synced with Clerk. 6 | 7 | The only thing that's happening here is syncing a basic `users` database table with the webhook incoming data. You can use this example as a starter to add any functionality you need. 8 | 9 | ## Data syncing 10 | 11 | Following a popular pattern for distributed systems syncing, Clerk uses webhooks to notify on important events. 12 | 13 | The recommended way to sync your datastore with Clerk is described in the following schema. The example is for signing up a new user, but the idea is the same for any other important event in your app. 14 | 15 | <img height="300px" src="../../../../../docs/widget-user-created-webhook.png" alt="New user webhook sync example" /> 16 | 17 | In the schema above there are three main steps. 18 | 19 | 1. A user signs up in your application. Your front-end will ask Clerk to handle the sign up. 20 | 2. Clerk replies back with the sign up response. The `SignUp` object and the new `User` object are available through the `Clerk`object in the front-end. 21 | 3. Clerk triggers a `user.created` webhook to your configured endpoint. The payload will hold all information regarding the newly created user. 22 | 23 | The webhook will be retried until Clerk receives a `200 OK` response from your server. 24 | 25 | ## Configuring webhooks 26 | 27 | Webhooks can be configured through your [Clerk Dashboard](https://dashboard.clerk.dev). 28 | 29 | Select your application instance and navigate to _Integrations_. Turn on the Svix integration. 30 | 31 | <img height="300px" src="../../../../../docs/widget-integrations-webhooks.png" alt="Enable webhooks integration" /> 32 | 33 | From there, you can set up endpoints to listen for all or specific Clerk events. 34 | 35 | In order for this example to work, once you've set up your webhooks integration from the Clerk Dashboard, you need to copy the webhook endpoint's signing secret and set it as the value of the `WEBHOOK_SECRET` environment variable. See the `.env` file included in the example. 36 | 37 | ## Testing locally 38 | 39 | Webhooks are designed to work with public URLs. As such, testing locally requires some additional effort. 40 | 41 | This section is about local testing, which is usually done only during development. Once you go to production, you'll need to replace your production public URLs in your webhook configuration. 42 | 43 | Testing webhooks locally requires some sort of public server that can tunnel requests to your localhost. Thankfully, there are some services and programs that offer this kind of functionality and they are easy to set up. 44 | 45 | The most popular service that provides a public URL to your localhost server is [ngrok](https://ngrok.com/). It's really easy to set up and it offers a free tier with limited usage. 46 | 47 | A popular ngrok alternative is [localtunnel](https://github.com/localtunnel/localtunnel). This is an open-source solution that can be installed as an npm package. 48 | 49 | ## Learn more 50 | 51 | To learn more about Clerk webhooks and Svix, the underlying webhook delivery service, take a look at the following links. 52 | 53 | - [Clerk webhook documentation](https://clerk.dev/docs/integration/webhooks) 54 | - [Receiving webhooks with Svix](https://docs.svix.com/receiving/introduction) 55 | - [Verifying Svix webhooks](https://docs.svix.com/receiving/verifying-payloads/how) 56 | -------------------------------------------------------------------------------- /examples/widget/pages/api/webhooks/user.ts: -------------------------------------------------------------------------------- 1 | import { IncomingHttpHeaders } from "http"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { Webhook, WebhookRequiredHeaders } from "svix"; 4 | import { buffer } from "micro"; 5 | import { upsert } from "../../../server/userRepo"; 6 | 7 | // Disable the bodyParser so we can access the raw 8 | // request body for verification. 9 | export const config = { 10 | api: { 11 | bodyParser: false, 12 | }, 13 | }; 14 | 15 | const webhookSecret: string = process.env.WEBHOOK_SECRET || ""; 16 | 17 | export default async function handler( 18 | req: NextApiRequestWithSvixRequiredHeaders, 19 | res: NextApiResponse 20 | ) { 21 | // Verify the webhook signature 22 | // See https://docs.svix.com/receiving/verifying-payloads/how 23 | const payload = (await buffer(req)).toString(); 24 | const headers = req.headers; 25 | const wh = new Webhook(webhookSecret); 26 | let evt: Event | null = null; 27 | try { 28 | evt = wh.verify(payload, headers) as Event; 29 | } catch (_) { 30 | return res.status(400).json({}); 31 | } 32 | 33 | // Handle the webhook 34 | const eventType: EventType = evt.type; 35 | if (eventType === "user.created" || eventType === "user.updated") { 36 | const { id, ...attributes } = evt.data; 37 | await upsert(id as string, attributes); 38 | } 39 | 40 | res.json({}); 41 | } 42 | 43 | type NextApiRequestWithSvixRequiredHeaders = NextApiRequest & { 44 | headers: IncomingHttpHeaders & WebhookRequiredHeaders; 45 | }; 46 | 47 | // Generic (and naive) way for the Clerk event 48 | // payload type. 49 | type Event = { 50 | data: Record<string, string | number>; 51 | object: "event"; 52 | type: EventType; 53 | }; 54 | 55 | type EventType = "user.created" | "user.updated" | "*"; 56 | -------------------------------------------------------------------------------- /examples/widget/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Dashboard } from "../components/Dashboard"; 2 | import type { NextPage } from "next"; 3 | 4 | const DashboardPage: NextPage = () => { 5 | return <Dashboard />; 6 | }; 7 | 8 | export default DashboardPage; 9 | -------------------------------------------------------------------------------- /examples/widget/pages/sign-in/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import {SignedOut} from "@clerk/nextjs"; 2 | import type {NextPage} from "next"; 3 | import {SignInLayout} from "../../layouts/SignInLayout"; 4 | import {SignInForm} from "../../components/SignInForm"; 5 | 6 | const SignIn: NextPage = () => { 7 | return ( 8 | <SignInLayout> 9 | <SignedOut> 10 | <SignInForm /> 11 | </SignedOut> 12 | </SignInLayout> 13 | ); 14 | }; 15 | 16 | export default SignIn; 17 | -------------------------------------------------------------------------------- /examples/widget/pages/sign-up/[[...index]].tsx: -------------------------------------------------------------------------------- 1 | import {SignedOut} from "@clerk/nextjs"; 2 | import type {NextPage} from "next"; 3 | import {SignUpLayout} from "../../layouts/SignUpLayout"; 4 | import {SignUpForm} from "../../components/SignUpForm"; 5 | 6 | const SignUp: NextPage = () => { 7 | return ( 8 | <SignUpLayout> 9 | <SignedOut> 10 | <SignUpForm /> 11 | </SignedOut> 12 | </SignUpLayout> 13 | ); 14 | }; 15 | 16 | export default SignUp; 17 | -------------------------------------------------------------------------------- /examples/widget/public/arrow-right.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#335bf1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"> 2 | <line x1="5" y1="12" x2="19" y2="12"></line> 3 | <polyline points="12 5 19 12 12 19"></polyline> 4 | </svg> 5 | -------------------------------------------------------------------------------- /examples/widget/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-nextjs-examples/9f6872c5cd732f1dc04e75eca778c15dc40ce3d6/examples/widget/public/favicon.png -------------------------------------------------------------------------------- /examples/widget/server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /examples/widget/server/userRepo.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../db/prisma"; 2 | import type { Prisma } from "@prisma/client"; 3 | 4 | export function upsert(externalId: string, attributes: Prisma.UserUpdateInput) { 5 | const create: Prisma.UserCreateInput = { externalId, attributes }; 6 | 7 | return prisma.user.upsert({ 8 | where: { externalId }, 9 | update: { attributes }, 10 | create, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /examples/widget/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | /* font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; */ 7 | font-family: "Inter", sans-serif; 8 | } 9 | 10 | a { 11 | color: inherit; 12 | text-decoration: none; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | } 18 | 19 | ul { 20 | padding: 0; 21 | margin: 0; 22 | list-style-type: none; 23 | } 24 | 25 | p { 26 | margin: 0; 27 | } 28 | 29 | button { 30 | outline: none; 31 | border: none; 32 | cursor: pointer; 33 | padding: 0; 34 | } 35 | 36 | footer { 37 | position: fixed; 38 | left: 0; 39 | right: 0; 40 | bottom: 0; 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | margin: 20px; 45 | } 46 | -------------------------------------------------------------------------------- /examples/widget/styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-primary: #296ae9; 3 | --color-error: #ff1d17; 4 | --color-black: #000000; 5 | --color-black-lighter: rgba(0, 0, 0, 0.8); 6 | --color-gray: rgba(0, 0, 0, 0.04); 7 | --color-white: #fff; 8 | --color-white-lighter: rgba(255, 255, 255, 0.8); 9 | } 10 | -------------------------------------------------------------------------------- /examples/widget/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------