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

View File

@@ -13,6 +13,7 @@ import { User } from 'db'
import { env, getAtPath, isDefined, isNotEmpty } from 'utils'
import { mockedUser } from '@/features/auth'
import { getNewUserInvitations } from '@/features/auth/api'
import { sendVerificationRequest } from './sendVerificationRequest'
const providers: Provider[] = []
@@ -42,6 +43,7 @@ if (isNotEmpty(env('SMTP_FROM')) && process.env.SMTP_AUTH_DISABLED !== 'true')
},
},
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 { MjmlButton } from '@faire/mjml-react'
import { IMjmlButtonProps, MjmlButton } from '@faire/mjml-react'
import { blue, grayLight } from '../theme'
import { leadingTight, textBase, borderBase } from '../theme'
type ButtonProps = {
link: string
children: React.ReactNode
}
} & IMjmlButtonProps
export const Button = ({ link, children }: ButtonProps) => (
export const Button = ({ link, children, ...props }: ButtonProps) => (
<MjmlButton
lineHeight={leadingTight}
fontSize={textBase}
@@ -20,6 +20,7 @@ export const Button = ({ link, children }: ButtonProps) => (
backgroundColor={blue}
color={grayLight}
borderRadius={borderBase}
{...props}
>
{children}
</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 './ReachedStorageLimitEmail'
export * from './WorkspaceMemberInvitationEmail'
export * from './MagicLinkEmail'

View File

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