23 |
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 |
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 |
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 |
115 | )}
116 | {formStep === SignInFormSteps.CODE && (
117 |
121 | )}
122 | {formStep === SignInFormSteps.PASSWORD && (
123 |
124 | )}
125 |
129 |
130 | );
131 | }
132 |
133 | type BackButtonProps = { onClick: () => void };
134 | function BackButton({ onClick }: BackButtonProps): JSX.Element {
135 | return (
136 |
137 |
140 |
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 |
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 |
26 |
32 |
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 |
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({ defaultValues: { country: "USA" } });
47 |
48 | if(!isLoaded) {
49 | return null;
50 | }
51 |
52 | const onSubmit: SubmitHandler = 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 |
96 |
102 | {formStep === SignUpFormSteps.FORM && (
103 |
156 | )}
157 | {formStep === SignUpFormSteps.CODE && (
158 |
162 | )}
163 |
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 |
6 |
7 | By creating an account, you agree to Widget’s{" "}
8 | Terms of Service and Privacy Policy
9 |
10 |
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
11 | >(({ children, isLoading = false, ...rest }, ref) => {
12 | return (
13 |
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 |
12 |
13 | {message}
14 |
15 |
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;
12 | autoFocus?: boolean;
13 | type?: string;
14 | badge?: React.ReactNode | string;
15 | } & React.InputHTMLAttributes
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 |
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 |
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 ;
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 |
12 |
{content}
13 |
{subtitle}
14 |
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 |
24 | Enter the 6-digit code sent to
25 | {emailAddress}
26 |
34 |
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 {children}
;
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 |
11 |
12 |
13 | {children}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/examples/widget/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/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 |
17 |
18 |
19 |
20 |
21 | {publicPages.includes(pathname) ? (
22 |
23 | ) : (
24 |
25 | )}
26 |
27 |
28 |
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 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
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 |
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;
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 ;
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 |
9 |
10 |
11 |
12 |
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 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SignUp;
17 |
--------------------------------------------------------------------------------
/examples/widget/public/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------