Skip to content

Commit

Permalink
feat: 🎸 export email textfield component, improve logic and doc
Browse files Browse the repository at this point in the history
  • Loading branch information
TimMikeladze committed May 30, 2022
1 parent 2a0a7f2 commit 9887480
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 59 deletions.
40 changes: 26 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<NextAuthDialog
open
ButtonProps={{
color: 'secondary',
}}
providers={{
google: {
label: 'Sign-in with Google',
},
{/* Render email field even if there is no email provider configured */}
alwaysShowEmailField
onSubmitEmail={async (email) => {
// Send magic link
const didToken = await magic.auth.loginWithMagicLink({ email });

// sign in with NextAuth
await signIn(`credentials`, {
didToken,
callbackUrl: router.query[`callbackUrl`]
});
}}
>
<TextField label="custom input" />
</NextAuthDialog>
/>
```

## All options
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
Expand Down Expand Up @@ -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
*/
Expand Down
62 changes: 20 additions & 42 deletions src/AuthDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Breakpoint,
Button,
ButtonProps,
CircularProgress,
Dialog,
DialogContent,
DialogContentProps,
Expand All @@ -18,7 +17,6 @@ import {
LinearProgress,
LinearProgressProps,
Stack,
TextField,
TextFieldProps,
Typography,
TypographyProps,
Expand All @@ -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 = {
/**
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
Expand Down Expand Up @@ -301,44 +305,18 @@ export function AuthDialog(props: AuthDialogProps) {
</DialogContentText>
<Stack spacing={2}>
{props.children}
{emailProviderConfig && (
<TextField
required
fullWidth
type="email"
autoFocus
value={email}
onChange={(e) => handleChangeEmail(e)}
onKeyDown={async (e) => {
if (e.key === 'Enter' && validEmail) {
await handleSubmitEmail();
}
}}
InputProps={{
startAdornment: (
<IconButton
disableRipple
sx={{
cursor: 'unset',
}}
>
<Icon icon="mdi:email-outline" />
</IconButton>
),
endAdornment: (
<IconButton disabled={!validEmail}>
{emailLoading ? (
<CircularProgress size={theme.spacing(3)} />
) : (
<Icon icon="mdi:send-outline" />
)}
</IconButton>
),
{(emailProviderConfig || props.alwaysShowEmailField) && (
<EmailField
onSubmitEmail={handleSubmitEmail}
onChangeEmail={handleChangeEmail}
email={email}
emailLoading={emailLoading}
emailProviderConfig={emailProviderConfig}
validEmail={validEmail}
TextFieldProps={{
...props.EmailFieldProps,
...emailProviderConfig?.EmailFieldProps,
}}
placeholder={emailProviderConfig.name || emailProviderConfig.placeholder || 'Email'}
helperText={emailProviderConfig.helperText || 'A sign-in link will be sent to your inbox.'}
{...props.TextFieldProps}
{...emailProviderConfig.TextFieldProps}
/>
)}
{noDivider ? null : <Divider>{dividerText}</Divider>}
Expand Down
60 changes: 60 additions & 0 deletions src/EmailField.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLTextAreaElement | HTMLInputElement>) => void;
onSubmitEmail: () => void;
validEmail?: boolean
}

export function EmailField(props: EmailFieldProps) {
const theme = useTheme();
return (
<TextField
required
fullWidth
type="email"
autoFocus
value={props.email}
onChange={(e) => props.onChangeEmail(e)}
onKeyDown={async (e) => {
if (e.key === 'Enter' && props.validEmail) {
await props.onSubmitEmail();
}
}}
InputProps={{
startAdornment: (
<IconButton
disableRipple
sx={{
cursor: 'unset',
}}
>
<Icon icon="mdi:email-outline" />
</IconButton>
),
endAdornment: (
<IconButton disabled={!props.validEmail} onClick={props.onSubmitEmail}>
{props.emailLoading ? (
<CircularProgress size={theme.spacing(3)} />
) : (
<Icon icon="mdi:send-outline" />
)}
</IconButton>
),
}}
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}
/>
);
}
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { NextAuthDialog } from './NextAuthDialog';
export * from './AuthDialog';
export * from './NextAuthDialog';
export * from './icons';
export * from './EmailField';

export default NextAuthDialog;
23 changes: 23 additions & 0 deletions src/stories/NextAuthDialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down
3 changes: 0 additions & 3 deletions src/stories/NextAuthDialogThemed.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ export const Default = Template.bind({});

Default.args = {
open: true,
DialogContentProps: {
children: 'foo',
},
};

Default.parameters = {
Expand Down

0 comments on commit 9887480

Please sign in to comment.