feat: optimize the onboarding subscription experience.

This commit is contained in:
Ahmed Bouhuolia
2024-04-15 12:48:16 +02:00
parent 9321db2a3a
commit 47d82ce591
28 changed files with 426 additions and 661 deletions

View File

@@ -54,7 +54,6 @@ function SetupLeftSectionHeader() {
<p className={'content__text'}>
<T id={'setup.left_side.description'} />
</p>
<div class="content__divider"></div>
<div className={'content__organization'}>
<span class="signout">

View File

@@ -29,10 +29,7 @@ function SetupRightSection({
}) {
return (
<section className={'setup-page__right-section'}>
<SetupWizardContent
setupStepId={setupStepId}
setupStepIndex={setupStepIndex}
/>
<SetupWizardContent stepId={setupStepId} stepIndex={setupStepIndex} />
<SetupDialogs />
</section>
);

View File

@@ -1,9 +0,0 @@
// @ts-nocheck
import React from 'react';
export default function SetupSteps({ step, children }) {
const activeStep = React.Children.toArray(children).filter(
(child) => child.props.id === step.id,
);
return activeStep;
}

View File

@@ -1,70 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Formik } from 'formik';
import * as R from 'ramda';
import '@/style/pages/Setup/Subscription.scss';
import SetupSubscriptionForm from './SetupSubscription/SetupSubscriptionForm';
import { getSubscriptionFormSchema } from './SubscriptionForm.schema';
import withSubscriptionPlansActions from '../Subscriptions/withSubscriptionPlansActions';
import { useGetLemonSqueezyCheckout } from '@/hooks/query/subscriptions';
/**
* Subscription step of wizard setup.
*/
function SetupSubscription({
// #withSubscriptionPlansActions
initSubscriptionPlans,
}) {
React.useEffect(() => {
initSubscriptionPlans();
}, [initSubscriptionPlans]);
React.useEffect(() => {
window.LemonSqueezy.Setup({
eventHandler: (event) => {
// Do whatever you want with this event data
if (event.event === 'Checkout.Success') {
}
},
});
}, []);
// Initial values.
const initialValues = {
plan_slug: 'essentials',
period: 'month',
license_code: '',
};
const { mutateAsync: getLemonCheckout } = useGetLemonSqueezyCheckout();
// Handle form submit.
const handleSubmit = (values) => {
getLemonCheckout({ variantId: '337977' })
.then((res) => {
const checkoutUrl = res.data.data.attributes.url;
window.LemonSqueezy.Url.Open(checkoutUrl);
})
.catch(() => {});
};
// Retrieve momerized subscription form schema.
const SubscriptionFormSchema = React.useMemo(
() => getSubscriptionFormSchema(),
[],
);
return (
<div className={'setup-subscription-form'}>
<Formik
validationSchema={SubscriptionFormSchema}
initialValues={initialValues}
component={SetupSubscriptionForm}
onSubmit={handleSubmit}
/>
</div>
);
}
export default R.compose(withSubscriptionPlansActions)(SetupSubscription);

View File

@@ -0,0 +1,5 @@
.root{
margin: 0 auto;
padding: 0 40px;
}

View File

@@ -0,0 +1,38 @@
// @ts-nocheck
import { useEffect } from 'react';
import * as R from 'ramda';
import { Box } from '@/components';
import { SubscriptionPlansSection } from './SubscriptionPlansSection';
import withSubscriptionPlansActions from '../../Subscriptions/withSubscriptionPlansActions';
import styles from './SetupSubscription.module.scss';
/**
* Subscription step of wizard setup.
*/
function SetupSubscription({
// #withSubscriptionPlansActions
initSubscriptionPlans,
}) {
useEffect(() => {
initSubscriptionPlans();
}, [initSubscriptionPlans]);
useEffect(() => {
window.LemonSqueezy.Setup({
eventHandler: (event) => {
// Do whatever you want with this event data
if (event.event === 'Checkout.Success') {
}
},
});
}, []);
return (
<Box className={styles.root}>
<SubscriptionPlansSection />
</Box>
);
}
export default R.compose(withSubscriptionPlansActions)(SetupSubscription);

View File

@@ -1,28 +0,0 @@
// @ts-nocheck
import { Form } from 'formik';
import SubscriptionPlansSection from './SubscriptionPlansSection';
import SubscriptionPeriodsSection from './SubscriptionPeriodsSection';
import { Button, Intent } from '@blueprintjs/core';
import { T } from '@/components';
function StepSubscriptionActions() {
return (
<div>
<Button type="submit" intent={Intent.PRIMARY} large={true}>
<T id={'submit_voucher'} />
</Button>
</div>
);
}
export default function SetupSubscriptionForm() {
return (
<Form>
<div class="billing-plans">
<SubscriptionPlansSection />
<SubscriptionPeriodsSection />
<StepSubscriptionActions />
</div>
</Form>
);
}

View File

@@ -1,20 +0,0 @@
// @ts-nocheck
import React from 'react';
import { T } from '@/components';
import { PaymentMethodTabs } from '../../Subscriptions/SubscriptionTabs';
export default ({ formik, title, description }) => {
return (
<section class="billing-plans__section">
<h1 className="title">
<T id={'setup.plans.payment_methods.title'} />
</h1>
<p className="paragraph">
<T id={'setup.plans.payment_methods.description'} />
</p>
<PaymentMethodTabs formik={formik} />
</section>
);
};

View File

@@ -1,48 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Field } from 'formik';
import * as R from 'ramda';
import { T, SubscriptionPeriods } from '@/components';
import withPlan from '../../Subscriptions/withPlan';
const SubscriptionPeriodsEnhanced = R.compose(
withPlan(({ plan }) => ({ plan })),
)(({ plan, ...restProps }) => {
// Can't continue if the current plan of the form not selected.
if (!plan) {
return null;
}
return <SubscriptionPeriods periods={plan.periods} {...restProps} />;
});
/**
* Billing periods.
*/
export default function SubscriptionPeriodsSection() {
return (
<section class="billing-plans__section">
<h1 class="title">
<T id={'setup.plans.select_period.title'} />
</h1>
<div class="description">
<p className="paragraph">
<T id={'setup.plans.select_period.description'} />
</p>
</div>
<Field name={'period'}>
{({ form: { setFieldValue, values }, field: { value } }) => (
<SubscriptionPeriodsEnhanced
planSlug={values.plan_slug}
selectedPeriod={value}
onPeriodSelect={(period) => {
setFieldValue('period', period);
}}
/>
)}
</Field>
</section>
);
}

View File

@@ -0,0 +1,78 @@
// @ts-nocheck
import { AppToaster, Group, T } from '@/components';
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
import { Intent } from '@blueprintjs/core';
import { PricingPlan } from '@/components/PricingPlan/PricingPlan';
interface SubscriptionPricingProps {
slug: string;
label: string;
description: string;
features?: Array<String>;
featured?: boolean;
price: string;
pricePeriod: string;
}
function SubscriptionPricing({
featured,
label,
description,
features,
price,
pricePeriod,
}: SubscriptionPricingProps) {
const { mutateAsync: getLemonCheckout, isLoading } =
useGetLemonSqueezyCheckout();
const handleClick = () => {
getLemonCheckout({ variantId: '337977' })
.then((res) => {
const checkoutUrl = res.data.data.attributes.url;
window.LemonSqueezy.Url.Open(checkoutUrl);
})
.catch(() => {
AppToaster.show({
message: 'Something went wrong!',
intent: Intent.DANGER,
});
});
};
return (
<PricingPlan featured={featured}>
{featured && <PricingPlan.Featured>Most Popular</PricingPlan.Featured>}
<PricingPlan.Header label={label} description={description} />
<PricingPlan.Price price={price} subPrice={pricePeriod} />
<PricingPlan.BuyButton loading={isLoading} onClick={handleClick}>
Subscribe
</PricingPlan.BuyButton>
<PricingPlan.Features>
{features?.map((feature) => (
<PricingPlan.FeatureLine>{feature}</PricingPlan.FeatureLine>
))}
</PricingPlan.Features>
</PricingPlan>
);
}
export function SubscriptionPlans({ plans }) {
return (
<Group spacing={18} noWrap align='stretch'>
{plans.map((plan, index) => (
<SubscriptionPricing
key={index}
slug={plan.slug}
label={plan.name}
description={plan.description}
features={plan.features}
featured={plan.featured}
price={plan.price}
pricePeriod={plan.pricePeriod}
/>
))}
</Group>
);
}

View File

@@ -1,42 +1,25 @@
// @ts-nocheck
import React from 'react';
import { Field } from 'formik';
import { T } from '@/components';
import { T, SubscriptionPlans } from '@/components';
import { compose } from '@/utils';
import { SubscriptionPlans } from './SubscriptionPlan';
import withPlans from '../../Subscriptions/withPlans';
import { compose } from '@/utils';
/**
* Billing plans.
*/
function SubscriptionPlansSection({ plans }) {
function SubscriptionPlansSectionRoot({ plans }) {
return (
<section class="billing-plans__section">
<h1 class="title">
<T id={'setup.plans.select_plan.title'} />
</h1>
<div class="description">
<p className="paragraph">
<T id={'setup.plans.select_plan.description'} />
</p>
</div>
<section>
<p className="paragraph" style={{ marginBottom: '1.2rem' }}>
<T id={'setup.plans.select_plan.description'} />
</p>
<Field name={'plan_slug'}>
{({ form: { setFieldValue }, field: { value } }) => (
<SubscriptionPlans
value={value}
plans={plans}
onSelect={(value) => {
setFieldValue('plan_slug', value);
}}
/>
)}
</Field>
<SubscriptionPlans plans={plans} />
</section>
);
}
export default compose(withPlans(({ plans }) => ({ plans })))(
SubscriptionPlansSection,
);
export const SubscriptionPlansSection = compose(
withPlans(({ plans }) => ({ plans })),
)(SubscriptionPlansSectionRoot);

View File

@@ -0,0 +1,3 @@
.items {
padding: 40px 40px 20px;
}

View File

@@ -1,30 +1,50 @@
// @ts-nocheck
import React from 'react';
import SetupSteps from './SetupSteps';
import WizardSetupSteps from './WizardSetupSteps';
import SetupSubscription from './SetupSubscription';
import SetupSubscription from './SetupSubscription/SetupSubscription';
import SetupOrganizationPage from './SetupOrganizationPage';
import SetupInitializingForm from './SetupInitializingForm';
import SetupCongratsPage from './SetupCongratsPage';
import { Stepper } from '@/components/Stepper';
import styles from './SetupWizardContent.module.scss';
interface SetupWizardContentProps {
stepIndex: number;
stepId: string;
}
/**
* Setup wizard content.
*/
export default function SetupWizardContent({ setupStepIndex, setupStepId }) {
export default function SetupWizardContent({
stepIndex,
stepId,
}: SetupWizardContentProps) {
return (
<div class="setup-page__content">
<WizardSetupSteps currentStep={setupStepIndex} />
<Stepper
active={stepIndex}
classNames={{
content: styles.content,
items: styles.items,
}}
>
<Stepper.Step label={'Subscription'}>
<SetupSubscription />
</Stepper.Step>
<div class="setup-page-form">
<SetupSteps step={{ id: setupStepId }}>
<SetupSubscription id="subscription" />
<Stepper.Step label={'Organization'}>
<SetupOrganizationPage id="organization" />
</Stepper.Step>
<Stepper.Step label={'Initiializing'}>
<SetupInitializingForm id={'initializing'} />
</Stepper.Step>
<Stepper.Step label={'Congrats'}>
<SetupCongratsPage id="congrats" />
</SetupSteps>
</div>
</Stepper.Step>
</Stepper>
</div>
);
}