🚸 Improve magic link sign in experience
New email and sign in feedback
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
16
apps/builder/src/pages/api/auth/sendVerificationRequest.ts
Normal file
16
apps/builder/src/pages/api/auth/sendVerificationRequest.ts
Normal 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`)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
55
packages/emails/src/emails/MagicLinkEmail.tsx
Normal file
55
packages/emails/src/emails/MagicLinkEmail.tsx
Normal 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'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,
|
||||
})
|
||||
@@ -5,3 +5,4 @@ export * from './GuestInvitationEmail'
|
||||
export * from './ReachedChatsLimitEmail'
|
||||
export * from './ReachedStorageLimitEmail'
|
||||
export * from './WorkspaceMemberInvitationEmail'
|
||||
export * from './MagicLinkEmail'
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user