🚸 Improve magic link sign in experience
New email and sign in feedback
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
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 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>
|
||||||
|
|||||||
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 './ReachedChatsLimitEmail'
|
||||||
export * from './ReachedStorageLimitEmail'
|
export * from './ReachedStorageLimitEmail'
|
||||||
export * from './WorkspaceMemberInvitationEmail'
|
export * from './WorkspaceMemberInvitationEmail'
|
||||||
|
export * from './MagicLinkEmail'
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user