2
0

🚸 Improve magic link sign in experience

New email and sign in feedback
This commit is contained in:
Baptiste Arnaud
2023-03-13 11:20:28 +01:00
parent 4ae9ea32e4
commit 48db171c1b
7 changed files with 129 additions and 37 deletions

View File

@@ -6,7 +6,10 @@ import {
HStack, HStack,
Text, Text,
Spinner, Spinner,
Tooltip, Alert,
Flex,
AlertIcon,
SlideFade,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { ChangeEvent, FormEvent, useEffect } from 'react' import React, { ChangeEvent, FormEvent, useEffect } from 'react'
import { useState } from 'react' import { useState } from 'react'
@@ -78,11 +81,6 @@ export const SignInForm = ({
}) })
} else { } else {
setIsMagicLinkSent(true) setIsMagicLinkSent(true)
showToast({
status: 'success',
title: 'Success!',
description: 'Check your inbox to sign in',
})
} }
setAuthLoading(false) setAuthLoading(false)
} }
@@ -103,40 +101,54 @@ export const SignInForm = ({
) )
return ( return (
<Stack spacing="4" w="330px"> <Stack spacing="4" w="330px">
<SocialLoginButtons providers={providers} /> {!isMagicLinkSent && (
{providers?.email && (
<> <>
<DividerWithText mt="6">Or with your email</DividerWithText> <SocialLoginButtons providers={providers} />
<HStack as="form" onSubmit={handleEmailSubmit}> {providers?.email && (
<Input <>
name="email" <DividerWithText mt="6">Or with your email</DividerWithText>
type="email" <HStack as="form" onSubmit={handleEmailSubmit}>
autoComplete="email" <Input
placeholder="email@company.com" name="email"
required type="email"
value={emailValue} autoComplete="email"
onChange={handleEmailChange} placeholder="email@company.com"
/> required
<Tooltip value={emailValue}
label="A sign in email was sent. Make sure to check your SPAM folder." onChange={handleEmailChange}
isDisabled={!isMagicLinkSent} />
> <Button
<Button type="submit"
type="submit" isLoading={
isLoading={ ['loading', 'authenticated'].includes(status) || authLoading
['loading', 'authenticated'].includes(status) || authLoading }
} isDisabled={isMagicLinkSent}
isDisabled={isMagicLinkSent} >
> Submit
Submit </Button>
</Button> </HStack>
</Tooltip> </>
</HStack> )}
</> </>
)} )}
{router.query.error && ( {router.query.error && (
<SignInError error={router.query.error.toString()} /> <SignInError error={router.query.error.toString()} />
)} )}
<SlideFade offsetY="20px" in={isMagicLinkSent} unmountOnExit>
<Flex>
<Alert status="success" w="100%">
<HStack>
<AlertIcon />
<Stack spacing={1}>
<Text fontWeight="semibold">
A magic link email was sent. 🪄
</Text>
<Text fontSize="sm">Make sure to check your SPAM folder.</Text>
</Stack>
</HStack>
</Alert>
</Flex>
</SlideFade>
</Stack> </Stack>
) )
} }

View File

@@ -13,6 +13,7 @@ import { User } from 'db'
import { env, getAtPath, isDefined, isNotEmpty } from 'utils' import { env, getAtPath, isDefined, isNotEmpty } from 'utils'
import { mockedUser } from '@/features/auth' import { mockedUser } from '@/features/auth'
import { getNewUserInvitations } from '@/features/auth/api' import { getNewUserInvitations } from '@/features/auth/api'
import { sendVerificationRequest } from './sendVerificationRequest'
const providers: Provider[] = [] const providers: Provider[] = []
@@ -42,6 +43,7 @@ if (isNotEmpty(env('SMTP_FROM')) && process.env.SMTP_AUTH_DISABLED !== 'true')
}, },
}, },
from: env('SMTP_FROM'), from: env('SMTP_FROM'),
sendVerificationRequest,
}) })
) )

View File

@@ -0,0 +1,16 @@
import { EmailConfig } from 'next-auth/providers/email'
import { sendMagicLinkEmail } from 'emails'
type Props = {
identifier: string
url: string
provider: Partial<Omit<EmailConfig, 'options'>>
}
export const sendVerificationRequest = async ({ identifier, url }: Props) => {
try {
await sendMagicLinkEmail({ url, to: identifier })
} catch (err) {
throw new Error(`Email(s) could not be sent`)
}
}

View File

@@ -1,14 +1,14 @@
import React from 'react' import React from 'react'
import { MjmlButton } from '@faire/mjml-react' import { IMjmlButtonProps, MjmlButton } from '@faire/mjml-react'
import { blue, grayLight } from '../theme' import { blue, grayLight } from '../theme'
import { leadingTight, textBase, borderBase } from '../theme' import { leadingTight, textBase, borderBase } from '../theme'
type ButtonProps = { type ButtonProps = {
link: string link: string
children: React.ReactNode children: React.ReactNode
} } & IMjmlButtonProps
export const Button = ({ link, children }: ButtonProps) => ( export const Button = ({ link, children, ...props }: ButtonProps) => (
<MjmlButton <MjmlButton
lineHeight={leadingTight} lineHeight={leadingTight}
fontSize={textBase} fontSize={textBase}
@@ -20,6 +20,7 @@ export const Button = ({ link, children }: ButtonProps) => (
backgroundColor={blue} backgroundColor={blue}
color={grayLight} color={grayLight}
borderRadius={borderBase} borderRadius={borderBase}
{...props}
> >
{children} {children}
</MjmlButton> </MjmlButton>

View File

@@ -0,0 +1,55 @@
import React, { ComponentProps } from 'react'
import {
Mjml,
MjmlBody,
MjmlSection,
MjmlColumn,
MjmlSpacer,
} from '@faire/mjml-react'
import { render } from '@faire/mjml-react/utils/render'
import { HeroImage, Text, Button, Head } from '../components'
import { SendMailOptions } from 'nodemailer'
import { sendEmail } from '../sendEmail'
type Props = {
url: string
}
export const MagicLinkEmail = ({ url }: Props) => (
<Mjml>
<Head />
<MjmlBody width={600}>
<MjmlSection padding="0">
<MjmlColumn>
<HeroImage src="https://s3.fr-par.scw.cloud/typebot/public/typebots/rxp84mn10va5iqek63enrg99/blocks/yfazs53p6coxe4u3tbbvkl0m" />
</MjmlColumn>
</MjmlSection>
<MjmlSection padding="0 24px" cssClass="smooth">
<MjmlColumn>
<Text>Here is your magic link 👇</Text>
<MjmlSpacer />
<Button link={url} align="center">
Click here to sign in
</Button>
<Text>
If you didn&apos;t request this, please ignore this email.
</Text>
<Text>
Best,
<br />- Typebot Team.
</Text>
</MjmlColumn>
</MjmlSection>
</MjmlBody>
</Mjml>
)
export const sendMagicLinkEmail = ({
to,
...props
}: Pick<SendMailOptions, 'to'> & ComponentProps<typeof MagicLinkEmail>) =>
sendEmail({
to,
subject: 'Sign in to Typebot',
html: render(<MagicLinkEmail {...props} />).html,
})

View File

@@ -5,3 +5,4 @@ export * from './GuestInvitationEmail'
export * from './ReachedChatsLimitEmail' export * from './ReachedChatsLimitEmail'
export * from './ReachedStorageLimitEmail' export * from './ReachedStorageLimitEmail'
export * from './WorkspaceMemberInvitationEmail' export * from './WorkspaceMemberInvitationEmail'
export * from './MagicLinkEmail'

View File

@@ -10,6 +10,7 @@ import {
ReachedStorageLimitEmail, ReachedStorageLimitEmail,
WorkspaceMemberInvitation, WorkspaceMemberInvitation,
} from './emails' } from './emails'
import { MagicLinkEmail } from './emails/MagicLinkEmail'
const createDistFolder = () => { const createDistFolder = () => {
const dist = path.resolve(__dirname, 'dist') const dist = path.resolve(__dirname, 'dist')
@@ -91,6 +92,10 @@ const createHtmlFile = () => {
/> />
).html ).html
) )
fs.writeFileSync(
path.resolve(__dirname, 'dist', 'magicLink.html'),
render(<MagicLinkEmail url={'https://app.typebot.io'} />).html
)
} }
createDistFolder() createDistFolder()