mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
feat: optimize the onboarding subscription experience.
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
.root{
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.items {
|
||||
padding: 40px 40px 20px;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user