2
0

🚸 New dedicated onboarding page

This commit is contained in:
Baptiste Arnaud
2023-07-25 16:12:40 +02:00
parent 283c55c1a4
commit 43555c171e
15 changed files with 263 additions and 183 deletions

View File

@ -10,3 +10,4 @@ NEXT_PUBLIC_E2E_TEST=
NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME=
NEXT_PUBLIC_UNSPLASH_APP_NAME=
NEXT_PUBLIC_UNSPLASH_ACCESS_KEY=
NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID=

View File

@ -628,3 +628,10 @@ export const BookIcon = (props: IconProps) => (
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</Icon>
)
export const ChevronLastIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="m7 18 6-6-6-6" />
<path d="M17 6v12" />
</Icon>
)

View File

@ -0,0 +1,183 @@
import { ChevronLastIcon } from '@/components/icons'
import {
Button,
Flex,
HStack,
StackProps,
VStack,
chakra,
useColorModeValue,
} from '@chakra-ui/react'
import { Standard } from '@typebot.io/nextjs'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import confetti from 'canvas-confetti'
import { useUser } from '@/features/account/hooks/useUser'
import { env, isEmpty } from '@typebot.io/lib'
import { useI18n } from '@/locales'
const totalSteps = 5
export const OnboardingPage = () => {
const t = useI18n()
const { push, replace } = useRouter()
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
const confettiCanon = useRef<confetti.CreateTypes>()
const { user, updateUser } = useUser()
const [currentStep, setCurrentStep] = useState<number>(user?.name ? 2 : 1)
const isNewUser =
user &&
new Date(user.createdAt as unknown as string).toDateString() ===
new Date().toDateString()
useEffect(() => {
initConfettis()
})
useEffect(() => {
if (!user?.createdAt) return
if (isNewUser === false || isEmpty(env('ONBOARDING_TYPEBOT_ID')))
replace('/typebots')
}, [isNewUser, replace, user?.createdAt])
const initConfettis = () => {
if (!confettiCanvaContainer.current || confettiCanon.current) return
confettiCanon.current = confetti.create(confettiCanvaContainer.current, {
resize: true,
useWorker: true,
})
}
const updateUserInfo = async (answer: {
message: string
blockId: string
}) => {
const isOtherItem = [
'cl126pv7n001o2e6dajltc4qz',
'saw904bfzgspmt0l24achtiy',
].includes(answer.blockId)
if (isOtherItem) return
const isName = answer.blockId === 'cl126820m000g2e6dfleq78bt'
const isCompany = answer.blockId === 'cl126jioz000v2e6dwrk1f2cb'
const isCategories = answer.blockId === 'cl126lb8v00142e6duv5qe08l'
if (isName) updateUser({ name: answer.message })
if (isCategories) {
const onboardingCategories = answer.message.split(', ')
updateUser({ onboardingCategories })
}
if (isCompany) {
updateUser({ company: answer.message })
if (confettiCanon.current) shootConfettis(confettiCanon.current)
}
setCurrentStep((prev) => prev + 1)
}
if (!isNewUser) return null
return (
<VStack h="100vh" flexDir="column" justifyContent="center" spacing="10">
<Button
rightIcon={<ChevronLastIcon />}
pos="fixed"
top="5"
right="5"
variant="ghost"
size="sm"
onClick={() => push('/typebots')}
>
{t('skip')}
</Button>
<Dots currentStep={currentStep} pos="fixed" top="9" />
<Flex w="full" maxW="800px" h="full" maxH="70vh" rounded="lg">
<Standard
typebot={env('ONBOARDING_TYPEBOT_ID')}
style={{ borderRadius: '1rem' }}
prefilledVariables={{ Name: user?.name, Email: user?.email }}
onEnd={() => {
setTimeout(() => {
push('/typebots/create', { query: { isFirstBot: true } })
}, 2000)
}}
onAnswer={updateUserInfo}
/>
</Flex>
<chakra.canvas
ref={confettiCanvaContainer}
pos="fixed"
top="0"
left="0"
w="full"
h="full"
zIndex={9999}
pointerEvents="none"
/>
</VStack>
)
}
const Dots = ({
currentStep,
...props
}: { currentStep: number } & StackProps) => {
const highlightedBgColor = useColorModeValue('gray.500', 'gray.100')
const baseBgColor = useColorModeValue('gray.200', 'gray.700')
return (
<HStack spacing="10" {...props}>
{Array.from({ length: totalSteps }).map((_, index) => (
<chakra.div
key={index}
boxSize={'2'}
bgColor={currentStep === index + 1 ? highlightedBgColor : baseBgColor}
rounded="full"
transition="background-color 0.2s ease"
/>
))}
</HStack>
)
}
const shootConfettis = (confettiCanon: confetti.CreateTypes) => {
const count = 200
const defaults = {
origin: { y: 0.7 },
}
const fire = (
particleRatio: number,
opts: {
spread: number
startVelocity?: number
decay?: number
scalar?: number
}
) => {
confettiCanon(
Object.assign({}, defaults, opts, {
particleCount: Math.floor(count * particleRatio),
})
)
}
fire(0.25, {
spread: 26,
startVelocity: 55,
})
fire(0.2, {
spread: 60,
})
fire(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
})
fire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
})
fire(0.1, {
spread: 120,
startVelocity: 45,
})
}

View File

@ -48,6 +48,22 @@ export const SignInPage = ({ type }: Props) => {
</Text>
)}
<SignInForm defaultEmail={query.g?.toString()} />
{type === 'signup' ? (
<Text fontSize="sm" maxW="400px" textAlign="center">
{scopedT('register.aggreeToTerms', {
termsOfService: (
<TextLink href={'https://typebot.io/terms-of-service'}>
{scopedT('register.termsOfService')}
</TextLink>
),
privacyPolicy: (
<TextLink href={'https://typebot.io/privacy-policies'}>
{scopedT('register.privacyPolicy')}
</TextLink>
),
})}
</Text>
) : null}
</VStack>
)
}

View File

@ -42,7 +42,6 @@ const parseVideoUrl = (
return { type: VideoBubbleContentType.VIMEO, url, id }
}
if (youtubeRegex.test(url)) {
console.log(url.match(youtubeRegex)?.at(2))
const id = url.match(youtubeRegex)?.at(2)
if (!id) return { type: VideoBubbleContentType.URL, url }
return { type: VideoBubbleContentType.YOUTUBE, url, id }

View File

@ -1,142 +0,0 @@
import { chakra, useColorModeValue } from '@chakra-ui/react'
import { Popup } from '@typebot.io/nextjs'
import { useUser } from '@/features/account/hooks/useUser'
import React, { useEffect, useRef, useState } from 'react'
import confetti from 'canvas-confetti'
import { useRouter } from 'next/router'
type Props = { totalTypebots: number }
export const OnboardingModal = ({ totalTypebots }: Props) => {
const { push } = useRouter()
const backgroundColor = useColorModeValue('white', '#171923')
const { user, updateUser } = useUser()
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
const confettiCanon = useRef<confetti.CreateTypes>()
const [chosenCategories, setChosenCategories] = useState<string[]>([])
const isNewUser =
user &&
new Date(user?.createdAt as unknown as string).toDateString() ===
new Date().toDateString() &&
totalTypebots === 0
useEffect(() => {
initConfettis()
}, [])
const initConfettis = () => {
if (!confettiCanvaContainer.current || confettiCanon.current) return
confettiCanon.current = confetti.create(confettiCanvaContainer.current, {
resize: true,
useWorker: true,
})
}
const handleBotEnd = () => {
setTimeout(() => {
push('/typebots/create', { query: { isFirstBot: true } })
}, 2000)
}
const handleNewAnswer = async (answer: {
message: string
blockId: string
}) => {
const isName = answer.blockId === 'cl126820m000g2e6dfleq78bt'
const isCompany = answer.blockId === 'cl126jioz000v2e6dwrk1f2cb'
const isCategories = answer.blockId === 'cl126lb8v00142e6duv5qe08l'
const isOtherCategories = answer.blockId === 'cl126pv7n001o2e6dajltc4qz'
if (isName) updateUser({ name: answer.message })
if (isCompany) {
updateUser({ company: answer.message })
if (confettiCanon.current) shootConfettis(confettiCanon.current)
}
if (isCategories) {
const onboardingCategories = answer.message.split(', ')
updateUser({ onboardingCategories })
setChosenCategories(onboardingCategories)
}
if (isOtherCategories)
updateUser({
onboardingCategories: [...chosenCategories, answer.message],
})
}
return (
<>
<chakra.canvas
ref={confettiCanvaContainer}
pos="fixed"
top="0"
left="0"
w="full"
h="full"
zIndex={9999}
pointerEvents="none"
/>
{user?.email && (
<Popup
typebot="onboarding-typebot"
prefilledVariables={{
Name: user.name?.split(' ')[0] ?? undefined,
Email: user.email ?? undefined,
}}
theme={{
backgroundColor,
zIndex: 100,
}}
defaultOpen={isNewUser}
onAnswer={handleNewAnswer}
onEnd={handleBotEnd}
/>
)}
</>
)
}
const shootConfettis = (confettiCanon: confetti.CreateTypes) => {
const count = 200
const defaults = {
origin: { y: 0.7 },
}
const fire = (
particleRatio: number,
opts: {
spread: number
startVelocity?: number
decay?: number
scalar?: number
}
) => {
confettiCanon(
Object.assign({}, defaults, opts, {
particleCount: Math.floor(count * particleRatio),
})
)
}
fire(0.25, {
spread: 26,
startVelocity: 55,
})
fire(0.2, {
spread: 60,
})
fire(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
})
fire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
})
fire(0.1, {
spread: 120,
startVelocity: 45,
})
}

View File

@ -12,7 +12,6 @@ import {
import { useTypebotDnd } from '../TypebotDndProvider'
import React, { useState } from 'react'
import { BackButton } from './BackButton'
import { OnboardingModal } from '../../dashboard/components/OnboardingModal'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { useFolders } from '../hooks/useFolders'
@ -26,7 +25,6 @@ import { TypebotCardOverlay } from './TypebotButtonOverlay'
import { useI18n } from '@/locales'
import { useTypebots } from '@/features/dashboard/hooks/useTypebots'
import { TypebotInDashboard } from '@/features/dashboard/types'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
type Props = { folder: DashboardFolder | null }
@ -161,9 +159,6 @@ export const FolderContent = ({ folder }: Props) => {
return (
<Flex w="full" flex="1" justify="center">
{typebots && isCloudProdInstance && (
<OnboardingModal totalTypebots={typebots.length} />
)}
<Stack w="1000px" spacing={6}>
<Skeleton isLoaded={folder?.name !== undefined}>
<Heading as="h1">{folder?.name}</Heading>

View File

@ -12,6 +12,7 @@ export default {
downgrade: 'Downgrade',
remove: 'Entfernen',
pending: 'Ausstehend',
skip: 'Überspringen',
'folders.createFolderButton.label': 'Ordner erstellen',
'folders.createTypebotButton.label': 'Typebot erstellen',
'folders.folderButton.deleteConfirmationMessage':
@ -77,6 +78,10 @@ export default {
'auth.register.alreadyHaveAccountLabel.preLink':
'Bereits ein Konto vorhanden?',
'auth.register.alreadyHaveAccountLabel.link': 'Anmelden',
'auth.register.aggreeToTerms':
'Durch die Registrierung stimmst du unseren {termsOfService} und {privacyPolicy} zu.',
'auth.register.termsOfService': 'Nutzungsbedingungen',
'auth.register.privacyPolicy': 'Datenschutzrichtlinie',
'auth.error.default': 'Versuche, dich mit einem anderen Konto anzumelden.',
'auth.error.email':
'E-Mail nicht gefunden. Versuche, dich mit einem anderen Anbieter anzumelden.',

View File

@ -12,6 +12,7 @@ export default {
downgrade: 'Downgrade',
remove: 'Remove',
pending: 'Pending',
skip: 'Skip',
'folders.createFolderButton.label': 'Create a folder',
'folders.createTypebotButton.label': 'Create a typebot',
'folders.folderButton.deleteConfirmationMessage':
@ -75,6 +76,10 @@ export default {
'auth.register.heading': 'Create an account',
'auth.register.alreadyHaveAccountLabel.preLink': 'Already have an account?',
'auth.register.alreadyHaveAccountLabel.link': 'Sign in',
'auth.register.aggreeToTerms':
'By signing up, you agree to our {termsOfService} and {privacyPolicy}.',
'auth.register.termsOfService': 'terms of service',
'auth.register.privacyPolicy': 'privacy policy',
'auth.error.default': 'Try signing with a different account.',
'auth.error.email': 'Email not found. Try signing with a different provider.',
'auth.error.oauthNotLinked':

View File

@ -12,6 +12,7 @@ export default {
downgrade: 'Downgrade',
remove: 'Retirer',
pending: 'En attente',
skip: 'Passer',
'folders.createFolderButton.label': 'Créer un dossier',
'folders.createTypebotButton.label': 'Créer un typebot',
'folders.folderButton.deleteConfirmationMessage':
@ -75,6 +76,10 @@ export default {
'auth.register.heading': 'Créer un compte',
'auth.register.alreadyHaveAccountLabel.preLink': 'Tu as déjà un compte?',
'auth.register.alreadyHaveAccountLabel.link': 'Se connecter',
'auth.register.aggreeToTerms':
'En vous inscrivant, vous acceptez nos {termsOfService} et {privacyPolicy}.',
'auth.register.termsOfService': "conditions d'utilisation",
'auth.register.privacyPolicy': 'politique de confidentialité',
'auth.error.default': 'Essaye de te connecter avec un compte différent.',
'auth.error.email':
'Email non trouvé. Essaye de te connecter avec un fournisseur différent.',

View File

@ -12,6 +12,7 @@ export default {
downgrade: 'Downgrade',
remove: 'Remover',
pending: 'Pendente',
skip: 'Pular',
'folders.createFolderButton.label': 'Criar uma pasta',
'folders.createTypebotButton.label': 'Criar um typebot',
'folders.folderButton.deleteConfirmationMessage':
@ -76,6 +77,10 @@ export default {
'auth.register.heading': 'Criar uma conta',
'auth.register.alreadyHaveAccountLabel.preLink': 'Já tem uma conta?',
'auth.register.alreadyHaveAccountLabel.link': 'Entrar',
'auth.register.aggreeToTerms':
'Ao se cadastrar, você concorda com nossos {termsOfService} e {privacyPolicy}.',
'auth.register.termsOfService': 'termos de serviço',
'auth.register.privacyPolicy': 'política de privacidade',
'auth.error.default': 'Tente entrar com uma conta diferente.',
'auth.error.email':
'E-mail não encontrado. Tente entrar com um provedor diferente.',

View File

@ -121,8 +121,8 @@ if (isNotEmpty(process.env.CUSTOM_OAUTH_WELL_KNOWN_URL)) {
type: 'oauth',
authorization: {
params: {
scope: process.env.CUSTOM_OAUTH_SCOPE ?? 'openid profile email'
}
scope: process.env.CUSTOM_OAUTH_SCOPE ?? 'openid profile email',
},
},
clientId: process.env.CUSTOM_OAUTH_CLIENT_ID,
clientSecret: process.env.CUSTOM_OAUTH_CLIENT_SECRET,
@ -156,6 +156,9 @@ export const authOptions: AuthOptions = {
},
pages: {
signIn: '/signin',
newUser: process.env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID
? '/onboarding'
: undefined,
},
callbacks: {
session: async ({ session, user }) => {

View File

@ -0,0 +1,5 @@
import { OnboardingPage } from '@/features/auth/components/OnboardingPage'
export default function Page() {
return <OnboardingPage />
}

View File

@ -21,6 +21,7 @@ Parameters marked with <Asterix/> are required.
| NEXTAUTH_URL_INTERNAL | | The internal builder base URL. You have to set it only when `NEXTAUTH_URL` can't be reached by your builder container / server. For a docker deployment, you should set it to `http://localhost:3000`. |
| DEFAULT_WORKSPACE_PLAN | FREE | Default workspace plan on user creation or when a user creates a new workspace. Possible values are `FREE`, `STARTER`, `PRO`, `LIFETIME`, `UNLIMITED`. The default plan for admin user is `UNLIMITED` |
| DISABLE_SIGNUP | false | Disable new user sign ups. Invited users are still able to sign up. |
| NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID | | Typebot ID used for the onboarding. Onboarding page is skipped if not provided. |
## Email (Auth, notifications)

View File

@ -14,12 +14,13 @@ const PrivacyPolicies = () => {
<Heading>1. Terms</Heading>
<p>
By accessing this Website, accessible from https://www.typebot.io, you
are agreeing to be bound by these Website Terms and Conditions of Use
and agree that you are responsible for the agreement with any
applicable local laws. If you disagree with any of these terms, you
are prohibited from accessing this site. The materials contained in
this Website are protected by copyright and trade mark law.
By accessing this Website, accessible from https://typebot.io and
https://app.typebot.io, you are agreeing to be bound by these Website
Terms and Conditions of Use and agree that you are responsible for the
agreement with any applicable local laws. If you disagree with any of
these terms, you are prohibited from accessing this site. The
materials contained in this Website are protected by copyright and
trade mark law.
</p>
<Heading>2. Use License</Heading>
@ -32,15 +33,6 @@ const PrivacyPolicies = () => {
</p>
<ul>
<li>modify or copy the materials;</li>
<li>
use the materials for any commercial purpose or for any public
display;
</li>
<li>
attempt to reverse engineer any software contained on Typebot&apos;s
Website;
</li>
<li>
remove any copyright or other proprietary notations from the
materials; or
@ -70,12 +62,12 @@ const PrivacyPolicies = () => {
<Heading>3. Disclaimer</Heading>
<p>
All the materials on Typebots Website are provided &quot;as is&quot;.
Typebot makes no warranties, may it be expressed or implied, therefore
negates all other warranties. Furthermore, Typebot does not make any
representations concerning the accuracy or reliability of the use of
the materials on its Website or otherwise relating to such materials
or any sites linked to this Website.
All the materials on Typebot&apos;s Website are provided &quot;as
is&quot;. Typebot makes no warranties, may it be expressed or implied,
therefore negates all other warranties. Furthermore, Typebot does not
make any representations concerning the accuracy or reliability of the
use of the materials on its Website or otherwise relating to such
materials or any sites linked to this Website.
</p>
<Heading>4. Limitations</Heading>
@ -83,22 +75,22 @@ const PrivacyPolicies = () => {
<p>
Typebot or its suppliers will not be hold accountable for any damages
that will arise with the use or inability to use the materials on
Typebots Website, even if Typebot or an authorize representative of
this Website has been notified, orally or written, of the possibility
of such damage. Some jurisdiction does not allow limitations on
implied warranties or limitations of liability for incidental damages,
these limitations may not apply to you.
Typebot&apos;s Website, even if Typebot or an authorize representative
of this Website has been notified, orally or written, of the
possibility of such damage. Some jurisdiction does not allow
limitations on implied warranties or limitations of liability for
incidental damages, these limitations may not apply to you.
</p>
<Heading>5. Revisions and Errata</Heading>
<p>
The materials appearing on Typebots Website may include technical,
typographical, or photographic errors. Typebot will not promise that
any of the materials in this Website are accurate, complete, or
current. Typebot may change the materials contained on its Website at
any time without notice. Typebot does not make any commitment to
update the materials.
The materials appearing on Typebot&apos;s Website may include
technical, typographical, or photographic errors. Typebot will not
promise that any of the materials in this Website are accurate,
complete, or current. Typebot may change the materials contained on
its Website at any time without notice. Typebot does not make any
commitment to update the materials.
</p>
<Heading>6. Links</Heading>
@ -107,7 +99,7 @@ const PrivacyPolicies = () => {
Typebot has not reviewed all of the sites linked to its Website and is
not responsible for the contents of any such linked site. The presence
of any link does not imply endorsement by Typebot of the site. The use
of any linked website is at the users own risk.
of any linked website is at the user&apos;s own risk.
</p>
<Heading>7. Site Terms of Use Modifications</Heading>