Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(onboarding): add new step pricing #425

Merged
merged 8 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 0 additions & 3 deletions libs/domains/organization/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,3 @@ export * from './lib/mocks/organizations-handler.mock'
export * from './lib/mocks/cluster-factory.mock'
export * from './lib/mocks/cluster-log-factory.mock'
export * from './lib/mocks/auth-provider.mock'
export * from './lib/interfaces/organization-plan.interface'
export * from './lib/interfaces/organization-price.interface'
export * from './lib/enums/organization-plan-type.enum'

This file was deleted.

This file was deleted.

14 changes: 6 additions & 8 deletions libs/domains/organization/src/lib/slices/organization.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,9 @@ export const fetchOrganizationById = createAsyncThunk(

export const postOrganization = createAsyncThunk<OrganizationEntity, OrganizationRequest>(
'organization/post',
async (data: OrganizationRequest, { rejectWithValue }) => {
try {
const result = await organizationMainCalls.createOrganization(data)
return result.data
} catch (err) {
return rejectWithValue(err)
}
async (data: OrganizationRequest) => {
const result = await organizationMainCalls.createOrganization(data)
return result.data
}
)

Expand Down Expand Up @@ -259,12 +255,14 @@ export const organizationSlice = createSlice({
state.loadingStatus = 'loading'
})
.addCase(postOrganization.fulfilled, (state: OrganizationState, action: PayloadAction<OrganizationEntity>) => {
organizationAdapter.setOne(state, action.payload)
organizationAdapter.addOne(state, action.payload)
state.loadingStatus = 'loaded'
toast(ToastEnum.SUCCESS, 'Your organization has been created')
})
.addCase(postOrganization.rejected, (state: OrganizationState, action) => {
state.loadingStatus = 'error'
state.error = action.error.message
toastError(action.error)
})
// delete organization
.addCase(deleteOrganization.pending, (state: OrganizationState) => {
Expand Down
13 changes: 2 additions & 11 deletions libs/pages/onboarding/src/lib/feature/container/container.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { createContext, useEffect, useState } from 'react'
import { Params, useNavigate } from 'react-router-dom'
import {
ONBOARDING_PRICING_FREE_URL,
ONBOARDING_PRICING_URL,
ONBOARDING_PROJECT_URL,
ONBOARDING_URL,
Route,
} from '@qovery/shared/router'
import { ONBOARDING_PRICING_URL, ONBOARDING_PROJECT_URL, Route } from '@qovery/shared/router'
import { ROUTER_ONBOARDING_STEP_1, ROUTER_ONBOARDING_STEP_2 } from '../../router/router'
import { LayoutOnboarding } from '../../ui/layout-onboarding/layout-onboarding'

Expand Down Expand Up @@ -40,10 +34,6 @@ export function Container(props: ContainerProps) {

useEffect(() => {
setStep(params['*'])

if (step === ONBOARDING_PRICING_URL.replace('/', '')) {
navigate(`${ONBOARDING_URL}${ONBOARDING_PRICING_FREE_URL}`)
}
}, [params, setStep, step, navigate])

const stepsNumber: number = firstStep ? ROUTER_ONBOARDING_STEP_1.length : ROUTER_ONBOARDING_STEP_2.length
Expand All @@ -68,6 +58,7 @@ export function Container(props: ContainerProps) {
stepsNumber={stepsNumber}
currentStepPosition={currentStepPosition(currentRoutes)}
step={step}
withoutRightContent={`/${params['*']}` === ONBOARDING_PRICING_URL}
catchline={
firstStep
? 'Just a few questions'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,86 +1,69 @@
import { Organization, PlanEnum, Project } from 'qovery-typescript-axios'
import { useContext, useEffect, useState } from 'react'
import { useContext, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { useIntercom } from 'react-use-intercom'
import {
OrganizationPlan,
OrganizationPlanType,
OrganizationPrice,
postOrganization,
} from '@qovery/domains/organization'
import { postOrganization } from '@qovery/domains/organization'
import { postProject } from '@qovery/domains/projects'
import { useAuth } from '@qovery/shared/auth'
import { ONBOARDING_PRICING_URL, ONBOARDING_PROJECT_URL, ONBOARDING_URL } from '@qovery/shared/router'
import { ENVIRONMENTS_GENERAL_URL, ENVIRONMENTS_URL } from '@qovery/shared/router'
import { useDocumentTitle } from '@qovery/shared/utils'
import { AppDispatch } from '@qovery/store'
import { StepPricing } from '../../ui/step-pricing/step-pricing'
import { ContextOnboarding } from '../container/container'

function listPrice(base: number, isBusinessPlan?: boolean) {
const results: OrganizationPrice[] = []
let multiple = 0

for (let i = 100; i <= 4000; i = i + 100) {
const nbDeploy = isBusinessPlan ? 1000 : 300

if (i > nbDeploy) multiple += 1
const price = i > nbDeploy ? base + 50 * multiple : base
results.push({
number: i.toString(),
price: price.toString(),
})
}
return results
export interface OrganizationPlan {
name: PlanEnum
title: string
text: string
price: number
list: string[]
}

const PLANS: OrganizationPlan[] = [
{
name: PlanEnum.FREE,
title: 'Free',
text: 'Adapted for personnal project',
title: 'Free plan',
text: 'Adapted for start',
price: 0,
listPrice: [],
list: [
'Deploy on your AWS account',
'Unlimited Developers',
'Up to 1 cluster',
'Up to 5 Environments',
'Preview Environment in one-click',
'Community support (forum)',
],
},
{
name: PlanEnum.TEAM,
title: 'Professional',
text: 'For 5-20 members',
title: 'Team plan',
text: 'Adapted to scale',
price: 49,
listPrice: listPrice(49, false),
list: [
'All FREE features',
'Up to 3 clusters',
'Unlimited Deployments',
'Unlimited Environments',
'24/5 support (email and chat)',
],
},
{
name: PlanEnum.ENTERPRISE,
title: 'Business',
text: 'For medium company',
price: 599,
listPrice: listPrice(599, true),
},
{
name: PlanEnum.ENTERPRISE,
title: 'Enterprise',
text: 'For large company',
price: 0,
listPrice: [],
title: 'Enterprise plan',
text: 'Adapted for 100+ team',
price: 899,
list: [
'All TEAM features',
'Unlimited Clusters',
'Role-Based Access Control',
'Extended security and compliance',
'Usage Report',
'Custom support',
],
},
]

const PLAN_DEFAULT: PlanEnum = PlanEnum.FREE
const DEPLOY_DEFAULT = 100

const DEFAULT_PRICE = {
[OrganizationPlanType.FREE]: { disable: false },
[OrganizationPlanType.PROFESSIONAL]: {
number: PLANS.find((p) => p.name === PlanEnum.TEAM)?.listPrice[0].number,
disable: false,
},
[OrganizationPlanType.BUSINESS]: {
number: PLANS.find((p) => p.name === PlanEnum.ENTERPRISE)?.listPrice[0].number,
disable: false,
},
[OrganizationPlanType.ENTERPRISE]: { disable: false },
}

export function OnboardingPricing() {
useDocumentTitle('Onboarding Pricing - Qovery')

Expand All @@ -89,89 +72,43 @@ export function OnboardingPricing() {
const { showNewMessages } = useIntercom()
const { organization_name, project_name } = useContext(ContextOnboarding)
const { createAuthCookies, getAccessTokenSilently } = useAuth()
const [selectPlan, setSelectPlan] = useState(PLAN_DEFAULT)
const [currentValue, setCurrentValue] = useState(DEFAULT_PRICE)
const [currentDeploy, setCurrentDeploy] = useState(DEPLOY_DEFAULT)
const [loading, setLoading] = useState(false)

useEffect(() => {
if (organization_name === '' && project_name === '') {
navigate(`${ONBOARDING_URL}${ONBOARDING_PROJECT_URL}`)
} else {
navigate(`${ONBOARDING_URL}${ONBOARDING_PRICING_URL}/${selectPlan.toLowerCase()}`)
}
}, [selectPlan, navigate, organization_name, project_name])

const chooseDeploy = (value: number | null) => {
if (value) {
setCurrentDeploy(value)

if (value > 100) {
if (selectPlan === PlanEnum.FREE) setSelectPlan(PlanEnum.TEAM)

setCurrentValue({
[OrganizationPlanType.FREE]: { disable: true },
[OrganizationPlanType.PROFESSIONAL]: { number: value.toString(), disable: false },
[OrganizationPlanType.BUSINESS]: { number: value.toString(), disable: false },
[OrganizationPlanType.ENTERPRISE]: { disable: false },
})
} else {
setCurrentValue({
[OrganizationPlanType.FREE]: { disable: false },
[OrganizationPlanType.PROFESSIONAL]: { number: value.toString(), disable: false },
[OrganizationPlanType.BUSINESS]: { number: value.toString(), disable: false },
[OrganizationPlanType.ENTERPRISE]: { disable: false },
})
}
}
}
const [loading, setLoading] = useState('')

const onSubmit = async () => {
setLoading(true)
const onSubmit = async (plan: PlanEnum) => {
setLoading(plan)

const organization: Organization = await dispatch(
await dispatch(
postOrganization({
name: organization_name,
plan: selectPlan,
plan: plan,
})
).unwrap()
// refresh token needed after created an organization
await getAccessTokenSilently({ ignoreCache: true })

if (organization) {
const project: Project = await dispatch(
postProject({ organizationId: organization.id, name: project_name })
).unwrap()

if (project) {
await createAuthCookies()
setLoading(false)

setTimeout(() => {
const url = `${process.env['NX_URL'] || 'https://console.qovery.com'}?redirectLoginV3`
window.location.replace(url)
}, 500)
}
} else {
setLoading(false)
}
)
.then(async (result) => {
// refresh token needed after created an organization
await getAccessTokenSilently({ ignoreCache: true })

const organization = result.payload as Organization

if (result.payload) {
const project: Project = await dispatch(
postProject({ organizationId: organization.id, name: project_name })
).unwrap()
if (project) {
await createAuthCookies()
setLoading('')
// redirect on the project page
navigate(ENVIRONMENTS_URL(organization.id, project.id) + ENVIRONMENTS_GENERAL_URL)
}
} else {
setLoading('')
}
})
.catch(() => setLoading(''))
}

const onClickContact = () => showNewMessages()

return (
<StepPricing
selectPlan={selectPlan}
setSelectPlan={setSelectPlan}
currentValue={currentValue}
plans={PLANS}
chooseDeploy={chooseDeploy}
currentDeploy={currentDeploy}
onSubmit={onSubmit}
loading={loading}
onClickContact={onClickContact}
/>
)
return <StepPricing plans={PLANS} onSubmit={onSubmit} loading={loading} onClickContact={onClickContact} />
}

export default OnboardingPricing
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ export function OnboardingThanks() {
const { update } = useIntercom()

useEffect(() => {
// if (process.env['NODE_ENV'] === 'production') {
// update user intercom
update({
email: userSignUp?.user_email,
name: `${userSignUp?.first_name} ${userSignUp?.last_name}`,
userId: user.sub,
})
// }
}, [user, userSignUp, update])

return (
Expand Down
2 changes: 1 addition & 1 deletion libs/pages/onboarding/src/lib/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const ROUTER_ONBOARDING_STEP_2: Route[] = [
component: <OnboardingProject />,
},
{
path: `${ONBOARDING_PRICING_URL}/:plan`,
path: ONBOARDING_PRICING_URL,
component: <OnboardingPricing />,
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export interface LayoutOnboardingProps {
step: string | undefined
catchline: string
routes: Route[]
withoutRightContent: boolean
}

export function LayoutOnboarding(props: LayoutOnboardingProps) {
const { children, currentStepPosition, stepsNumber, getProgressPercentValue, step, catchline } = props
const { children, currentStepPosition, stepsNumber, getProgressPercentValue, step, catchline, withoutRightContent } =
props

return (
<main className="layout-onboarding h-full min-h-screen bg-white">
Expand All @@ -29,14 +31,18 @@ export function LayoutOnboarding(props: LayoutOnboardingProps) {
</div>
}
/>
<div className="flex h-full min-h-screen max-w-screen-2xl ml-auto mr-auto relative">
<div className="flex-[2_1_0%] px-4 md:px-24 bg-white">
<div className="max-w-lg mt-36 mx-auto">{children}</div>
</div>
<div className="hidden xl:block flex-[1_1_0%] pl-20 bg-element-light-lighter-300 overflow-hidden max-w-2xl before:absolute before:top-0 before:w-full before:h-full before:bg-element-light-lighter-300">
<OnboardingRightContent step={step} />
{withoutRightContent ? (
<div className="relative top-16 pt-14 px-4 flex justify-center">{children}</div>
) : (
<div className="flex h-full min-h-screen max-w-screen-2xl ml-auto mr-auto relative">
<div className="flex-[2_1_0%] px-4 md:px-24 bg-white">
<div className="max-w-lg mt-36 mx-auto">{children}</div>
</div>
<div className="hidden xl:block flex-[1_1_0%] pl-20 bg-element-light-lighter-300 overflow-hidden max-w-2xl before:absolute before:top-0 before:w-full before:h-full before:bg-element-light-lighter-300">
<OnboardingRightContent step={step} />
</div>
</div>
</div>
)}
</main>
)
}
Expand Down