From 98874804adbdc7d3ab1119e0e3014b4389f721f1 Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 30 May 2022 23:11:46 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20export=20email=20textfie?= =?UTF-8?q?ld=20component,=20improve=20logic=20and=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 40 ++++++++----- src/AuthDialog.tsx | 62 +++++++------------- src/EmailField.tsx | 60 +++++++++++++++++++ src/index.tsx | 1 + src/stories/NextAuthDialog.stories.tsx | 23 ++++++++ src/stories/NextAuthDialogThemed.stories.tsx | 3 - 6 files changed, 130 insertions(+), 59 deletions(-) create mode 100644 src/EmailField.tsx diff --git a/README.md b/README.md index 407e199..95a2112 100644 --- a/README.md +++ b/README.md @@ -47,26 +47,33 @@ Components rendered within the `next-auth-mui` dialog are customizable through p If you need to implement custom logic for fetching providers or want complete control over the sign-in dialog, you can also import the `AuthDialog` component. + ```tsx import { AuthDialog } from 'next-auth-mui'; ``` -Below is a simple customization to change the way buttons appear globally, provide a custom label to the Google sign-in button and render a custom input field. +### Custom email authentication + +A common use-case is using a 3rd party email authentication service to send magic links alongside NextAuth's OAuth providers. In this case you can implement a custom email submit handler. + +In the example below we're using [magic.link](https://magic.link/) to send emails and a custom credentials provider to authenticate the user. ```tsx { + // Send magic link + const didToken = await magic.auth.loginWithMagicLink({ email }); + + // sign in with NextAuth + await signIn(`credentials`, { + didToken, + callbackUrl: router.query[`callbackUrl`] + }); }} -> - - +/> ``` ## All options @@ -108,6 +115,10 @@ export type AuthDialogProps = PropsWithChildren<{ * See @mui/material documentation */ DialogTitleProps?: DialogTitleProps; + /** + * Props passed to the email input field. See @mui/material documentation + */ + EmailFieldProps?: TextFieldProps; /** * Props to pass to the default loading indicator. See @mui/material documentation */ @@ -117,9 +128,10 @@ export type AuthDialogProps = PropsWithChildren<{ */ Progress?: React.ReactNode; /** - * Props passed to the email input field. See @mui/material documentation + * Always show the email field regardless if email provider has been configured. + * This is useful for implementing email auth with a 3rd party api. */ - TextFieldProps?: TextFieldProps; + alwaysShowEmailField?: boolean; /** * Controls width of dialog. * When breakpoint >= viewport the dialog will be rendered in mobile mode. @@ -224,7 +236,7 @@ export type EmailProviderConfig = { /** * Override props passed to the email's input field. See @mui/material documentation. */ - TextFieldProps?: TextFieldProps, + EmailFieldProps?: TextFieldProps, /** * Override end icon rendered in the email input field */ diff --git a/src/AuthDialog.tsx b/src/AuthDialog.tsx index 0d3eef8..10a5c76 100644 --- a/src/AuthDialog.tsx +++ b/src/AuthDialog.tsx @@ -4,7 +4,6 @@ import { Breakpoint, Button, ButtonProps, - CircularProgress, Dialog, DialogContent, DialogContentProps, @@ -18,7 +17,6 @@ import { LinearProgress, LinearProgressProps, Stack, - TextField, TextFieldProps, Typography, TypographyProps, @@ -29,6 +27,7 @@ import { Icon, IconProps } from '@iconify/react'; import { signIn } from 'next-auth/react'; import { SignInOptions, SignInResponse } from 'next-auth/react/types'; import { icons } from './icons'; +import { EmailField } from './EmailField'; export type OauthProviderConfig = { /** @@ -65,7 +64,7 @@ export type EmailProviderConfig = { /** * Override props passed to the email's input field. See @mui/material documentation. */ - TextFieldProps?: TextFieldProps, + EmailFieldProps?: TextFieldProps, /** * Override end icon rendered in the email input field */ @@ -129,6 +128,10 @@ export type AuthDialogProps = PropsWithChildren<{ * See @mui/material documentation */ DialogTitleProps?: DialogTitleProps; + /** + * Props passed to the email input field. See @mui/material documentation + */ + EmailFieldProps?: TextFieldProps; /** * Props to pass to the default loading indicator. See @mui/material documentation */ @@ -138,9 +141,10 @@ export type AuthDialogProps = PropsWithChildren<{ */ Progress?: React.ReactNode; /** - * Props passed to the email input field. See @mui/material documentation + * Always show the email field regardless if email provider has been configured. + * This is useful for implementing email auth with a 3rd party api. */ - TextFieldProps?: TextFieldProps; + alwaysShowEmailField?: boolean; /** * Controls width of dialog. * When breakpoint >= viewport the dialog will be rendered in mobile mode. @@ -301,44 +305,18 @@ export function AuthDialog(props: AuthDialogProps) { {props.children} - {emailProviderConfig && ( - handleChangeEmail(e)} - onKeyDown={async (e) => { - if (e.key === 'Enter' && validEmail) { - await handleSubmitEmail(); - } - }} - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: ( - - {emailLoading ? ( - - ) : ( - - )} - - ), + {(emailProviderConfig || props.alwaysShowEmailField) && ( + )} {noDivider ? null : {dividerText}} diff --git a/src/EmailField.tsx b/src/EmailField.tsx new file mode 100644 index 0000000..219dfcd --- /dev/null +++ b/src/EmailField.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { + CircularProgress, IconButton, TextField, useTheme, TextFieldProps, +} from '@mui/material'; +import { Icon } from '@iconify/react'; +import { ProviderConfig } from './AuthDialog'; + +export type EmailFieldProps = { + TextFieldProps?: TextFieldProps, + email: string; + emailLoading?: boolean, + emailProviderConfig?: ProviderConfig, + onChangeEmail: (email: React.ChangeEvent) => void; + onSubmitEmail: () => void; + validEmail?: boolean +} + +export function EmailField(props: EmailFieldProps) { + const theme = useTheme(); + return ( + props.onChangeEmail(e)} + onKeyDown={async (e) => { + if (e.key === 'Enter' && props.validEmail) { + await props.onSubmitEmail(); + } + }} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: ( + + {props.emailLoading ? ( + + ) : ( + + )} + + ), + }} + placeholder={props.emailProviderConfig?.name || props.emailProviderConfig?.placeholder || 'Email'} + helperText={props.emailProviderConfig?.helperText || 'A sign-in link will be sent to your inbox.'} + {...props.TextFieldProps} + {...props.emailProviderConfig?.EmailFieldProps} + /> + ); +} diff --git a/src/index.tsx b/src/index.tsx index a78bd4b..c3eb1d5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,5 +3,6 @@ import { NextAuthDialog } from './NextAuthDialog'; export * from './AuthDialog'; export * from './NextAuthDialog'; export * from './icons'; +export * from './EmailField'; export default NextAuthDialog; diff --git a/src/stories/NextAuthDialog.stories.tsx b/src/stories/NextAuthDialog.stories.tsx index 5363522..cecd37f 100644 --- a/src/stories/NextAuthDialog.stories.tsx +++ b/src/stories/NextAuthDialog.stories.tsx @@ -3,6 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; import withMock from 'storybook-addon-mock'; import { TextField, Typography } from '@mui/material'; +import { action } from '@storybook/addon-actions'; import { NextAuthDialog } from '..'; import { allProviders, defaultProviders, emailProvider, getMockResponse, oauthProviders, @@ -97,6 +98,28 @@ Error.parameters = { }], }; +export const CustomEmailSubmit = Template.bind({}); + +CustomEmailSubmit.args = { + open: true, + alwaysShowEmailField: true, + EmailFieldProps: { + helperText: 'A custom function will run when email is submitted', + }, + onSubmitEmail: async (email) => { + await new Promise((resolve) => { + setTimeout(resolve, 3000); + }); + action('onSubmitEmail')(email); + }, +}; + +CustomEmailSubmit.parameters = { + mockData: [ + getMockResponse({}), + ], +}; + export const AllProviders = Template.bind({}); AllProviders.args = { diff --git a/src/stories/NextAuthDialogThemed.stories.tsx b/src/stories/NextAuthDialogThemed.stories.tsx index 4388696..49f301c 100644 --- a/src/stories/NextAuthDialogThemed.stories.tsx +++ b/src/stories/NextAuthDialogThemed.stories.tsx @@ -41,9 +41,6 @@ export const Default = Template.bind({}); Default.args = { open: true, - DialogContentProps: { - children: 'foo', - }, }; Default.parameters = {