mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
Merge pull request #404 from bigcapitalhq/optimize-ui-onboarding
feat: optimize the onboarding subscription experience.
This commit is contained in:
@@ -48,7 +48,7 @@ const GroupStyled = styled(Box)`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: ${(props: GroupProps) => (props.align || 'center')};
|
||||
flex-wrap: ${(props: GroupProps) => (props.noWrap ? 'nowrap' : 'wrap')};
|
||||
justify-content: ${(props: GroupProps) =>
|
||||
GROUP_POSITIONS[props.position || 'left']};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
.root{
|
||||
border-radius: 5px;
|
||||
padding: 40px 15px;
|
||||
position: relative;
|
||||
border: 1px solid #D8DEE4;
|
||||
padding-top: 45px;
|
||||
|
||||
&.isFeatured {
|
||||
background-color: #F5F6F8;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
.featuredBox {
|
||||
background-color: #A3ACBA;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2F343C;
|
||||
|
||||
}
|
||||
.description{
|
||||
font-size: 14px;
|
||||
color: #687385;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.buttonCTA {
|
||||
min-height: 34px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.features {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.priceRoot{
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.price {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #404854;
|
||||
}
|
||||
|
||||
.pricePer{
|
||||
color: #738091;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
125
packages/webapp/src/components/PricingPlan/PricingPlan.tsx
Normal file
125
packages/webapp/src/components/PricingPlan/PricingPlan.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Button, ButtonProps, Intent } from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
import { Box, Group, Stack } from '../Layout';
|
||||
import styles from './PricingPlan.module.scss';
|
||||
import { CheckCircled } from '@/icons/CheckCircled';
|
||||
|
||||
export interface PricingPlanProps {
|
||||
featured?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a pricing plan.
|
||||
* @param featured - Whether the plan is featured.
|
||||
* @param children - The content of the plan.
|
||||
*/
|
||||
export const PricingPlan = ({ featured, children }: PricingPlanProps) => {
|
||||
return (
|
||||
<Stack
|
||||
spacing={8}
|
||||
className={clsx(styles.root, { [styles.isFeatured]: featured })}
|
||||
>
|
||||
<>{children}</>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a featured section within a pricing plan.
|
||||
* @param children - The content of the featured section.
|
||||
*/
|
||||
PricingPlan.Featured = ({ children }: { children: React.ReactNode }) => {
|
||||
return <Box className={styles.featuredBox}>{children}</Box>;
|
||||
};
|
||||
|
||||
export interface PricingHeaderProps {
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the header of a pricing plan.
|
||||
* @param label - The label of the plan.
|
||||
* @param description - The description of the plan.
|
||||
*/
|
||||
PricingPlan.Header = ({ label, description }: PricingHeaderProps) => {
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<h4 className={styles.label}>{label}</h4>
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PricingPriceProps {
|
||||
price: string;
|
||||
subPrice: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the price of a pricing plan.
|
||||
* @param price - The main price of the plan.
|
||||
* @param subPrice - The sub-price of the plan.
|
||||
*/
|
||||
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
|
||||
return (
|
||||
<Stack spacing={6} className={styles.priceRoot}>
|
||||
<h4 className={styles.price}>{price}</h4>
|
||||
<span className={styles.pricePer}>{subPrice}</span>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PricingBuyButtonProps extends ButtonProps {}
|
||||
|
||||
/**
|
||||
* Displays a buy button within a pricing plan.
|
||||
* @param children - The content of the button.
|
||||
* @param props - Additional button props.
|
||||
*/
|
||||
PricingPlan.BuyButton = ({ children, ...props }: PricingBuyButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
{...props}
|
||||
fill={true}
|
||||
className={styles.buttonCTA}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PricingFeaturesProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a list of features within a pricing plan.
|
||||
* @param children - The list of features.
|
||||
*/
|
||||
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
||||
return (
|
||||
<Stack spacing={10} className={styles.features}>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PricingFeatureLineProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a single feature line within a list of features.
|
||||
* @param children - The content of the feature line.
|
||||
*/
|
||||
PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => {
|
||||
return (
|
||||
<Group noWrap spacing={12}>
|
||||
<CheckCircled height={12} width={12} />
|
||||
<Box className={styles.featureItem}>{children}</Box>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -1,136 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { T } from '@/components';
|
||||
import { saveInvoke } from '@/utils';
|
||||
|
||||
import '@/style/pages/Subscription/PlanRadio.scss';
|
||||
import '@/style/pages/Subscription/PlanPeriodRadio.scss';
|
||||
|
||||
export function SubscriptionPlans({ value, plans, onSelect }) {
|
||||
const handleSelect = (value) => {
|
||||
onSelect && onSelect(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'plan-radios'}>
|
||||
{plans.map((plan) => (
|
||||
<SubscriptionPlan
|
||||
name={plan.name}
|
||||
description={plan.description}
|
||||
slug={plan.slug}
|
||||
price={plan.price}
|
||||
currencyCode={plan.currencyCode}
|
||||
value={plan.slug}
|
||||
onSelected={handleSelect}
|
||||
selectedOption={value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubscriptionPlan({
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
currencyCode,
|
||||
|
||||
value,
|
||||
selectedOption,
|
||||
onSelected,
|
||||
}) {
|
||||
const handlePlanClick = () => {
|
||||
saveInvoke(onSelected, value);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
id={'basic-plan'}
|
||||
className={classNames('plan-radio', {
|
||||
'is-selected': selectedOption === value,
|
||||
})}
|
||||
onClick={handlePlanClick}
|
||||
>
|
||||
<div className={'plan-radio__header'}>
|
||||
<div className={'plan-radio__name'}>{name}</div>
|
||||
</div>
|
||||
|
||||
<div className={'plan-radio__description'}>
|
||||
<ul>
|
||||
{description.map((line) => (
|
||||
<li>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={'plan-radio__price'}>
|
||||
<span className={'plan-radio__amount'}>
|
||||
{price} {currencyCode}
|
||||
</span>
|
||||
<span className={'plan-radio__period'}>
|
||||
<T id={'monthly'} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription periods.
|
||||
*/
|
||||
export function SubscriptionPeriods({ periods, selectedPeriod, onPeriodSelect }) {
|
||||
const handleSelected = (value) => {
|
||||
saveInvoke(onPeriodSelect, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'plan-periods'}>
|
||||
{periods.map((period) => (
|
||||
<SubscriptionPeriod
|
||||
period={period.slug}
|
||||
label={period.label}
|
||||
onSelected={handleSelected}
|
||||
price={period.price}
|
||||
selectedPeriod={selectedPeriod}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Billing period.
|
||||
*/
|
||||
export function SubscriptionPeriod({
|
||||
// #ownProps
|
||||
label,
|
||||
selectedPeriod,
|
||||
onSelected,
|
||||
period,
|
||||
price,
|
||||
currencyCode,
|
||||
}) {
|
||||
const handlePeriodClick = () => {
|
||||
saveInvoke(onSelected, period);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
id={`plan-period-${period}`}
|
||||
className={classNames(
|
||||
{ 'is-selected': period === selectedPeriod },
|
||||
'period-radio',
|
||||
)}
|
||||
onClick={handlePeriodClick}
|
||||
>
|
||||
<span className={'period-radio__label'}>{label}</span>
|
||||
|
||||
<div className={'period-radio__price'}>
|
||||
<span className={'period-radio__amount'}>
|
||||
{price} {currencyCode}
|
||||
</span>
|
||||
<span className={'period-radio__period'}>{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ export * from './PdfPreview';
|
||||
export * from './Details';
|
||||
export * from './TotalLines/index';
|
||||
export * from './Alert';
|
||||
export * from './Subscriptions';
|
||||
export * from './Dashboard';
|
||||
export * from './Drawer';
|
||||
export * from './Forms';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export const useOrganizationSubscriptions = (props) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the checkout url of the lemon squeezy.
|
||||
* Fetches the checkout url of the Lemon Squeezy.
|
||||
*/
|
||||
export const useGetLemonSqueezyCheckout = (props = {}) => {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
26
packages/webapp/src/icons/CheckCircled.tsx
Normal file
26
packages/webapp/src/icons/CheckCircled.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props extends React.SVGProps<SVGSVGElement> {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const CheckCircled = ({ width, height, ...props }: Props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<g fill="#3FA40D" fillRule="evenodd" clipPath="url(#a)" clipRule="evenodd">
|
||||
<path d="M9.21 3.915a.562.562 0 0 1 0 .795L5.647 8.272a.563.563 0 0 1-.795 0L2.978 6.397a.562.562 0 0 1 .796-.795L5.25 7.08l3.165-3.165a.563.563 0 0 1 .795 0Z" />
|
||||
<path d="M6 10.875A4.875 4.875 0 0 0 10.875 6 4.87 4.87 0 0 0 6 1.125a4.875 4.875 0 1 0 0 9.75ZM6 12a6 6 0 0 0 6-6c0-3.314-2.678-6-6-6a6 6 0 0 0 0 12Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h12v12H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -1093,13 +1093,6 @@ export const getDashboardRoutes = () => [
|
||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||
},
|
||||
|
||||
// Subscription billing.
|
||||
{
|
||||
path: `/billing`,
|
||||
component: lazy(() => import('@/containers/Subscriptions/BillingForm')),
|
||||
breadcrumb: intl.get('new_billing'),
|
||||
subscriptionInactive: [SUBSCRIPTION_TYPE.MAIN],
|
||||
},
|
||||
// Payment modes.
|
||||
{
|
||||
path: `/payment-mades/import`,
|
||||
|
||||
@@ -26,7 +26,7 @@ export default (mapState) => {
|
||||
const mapped = {
|
||||
...condits,
|
||||
setupStepId: setupStep?.step,
|
||||
setupStepIndex: scenarios.indexOf(setupStep) + 1,
|
||||
setupStepIndex: scenarios.indexOf(setupStep),
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
|
||||
@@ -3,22 +3,13 @@ import { createReducer } from '@reduxjs/toolkit';
|
||||
import intl from 'react-intl-universal';
|
||||
import t from '@/store/types';
|
||||
|
||||
const getSubscriptionPeriods = () => [
|
||||
{
|
||||
slug: 'month',
|
||||
label: intl.get('plan.monthly'),
|
||||
},
|
||||
{
|
||||
slug: 'year',
|
||||
label: intl.get('plan.yearly'),
|
||||
},
|
||||
];
|
||||
|
||||
const getSubscriptionPlans = () => [
|
||||
{
|
||||
name: intl.get('plan.capital_basic.title'),
|
||||
slug: 'capital_basic',
|
||||
description: [
|
||||
description:
|
||||
'Manage recurring and one-time billing, including subscriptions and invoices.',
|
||||
features: [
|
||||
intl.get('plan.feature.sales_invoices'),
|
||||
intl.get('plan.feature.sales_estimates'),
|
||||
intl.get('plan.feature.customers'),
|
||||
@@ -27,25 +18,15 @@ const getSubscriptionPlans = () => [
|
||||
intl.get('plan.feature.expenses_tracking'),
|
||||
intl.get('plan.feature.basic_financial_reports'),
|
||||
],
|
||||
price: '55',
|
||||
periods: [
|
||||
{
|
||||
slug: 'month',
|
||||
label: intl.get('plan.monthly'),
|
||||
price: '55',
|
||||
},
|
||||
{
|
||||
slug: 'year',
|
||||
label: intl.get('plan.yearly'),
|
||||
price: '595',
|
||||
},
|
||||
],
|
||||
currencyCode: 'LYD',
|
||||
price: '$29',
|
||||
pricePeriod: 'Per Year',
|
||||
},
|
||||
{
|
||||
name: intl.get('plan.capital_plus.title'),
|
||||
slug: 'capital_plus',
|
||||
description: [
|
||||
description:
|
||||
'Manage recurring and one-time billing, including subscriptions and invoices.',
|
||||
features: [
|
||||
intl.get('plan.feature.all_capital_basic'),
|
||||
intl.get('plan.feature.predefined_user_roles'),
|
||||
intl.get('plan.feature.custom_tables_views'),
|
||||
@@ -53,25 +34,16 @@ const getSubscriptionPlans = () => [
|
||||
intl.get('plan.feature.plus_financial_reports'),
|
||||
intl.get('plan.feature.custom_fields_resources'),
|
||||
],
|
||||
price: '75',
|
||||
periods: [
|
||||
{
|
||||
slug: 'month',
|
||||
label: intl.get('plan.monthly'),
|
||||
price: '75',
|
||||
},
|
||||
{
|
||||
slug: 'year',
|
||||
label: intl.get('plan.yearly'),
|
||||
price: '795',
|
||||
},
|
||||
],
|
||||
currencyCode: 'LYD',
|
||||
price: '$29',
|
||||
pricePeriod: 'Per Year',
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
name: intl.get('plan.essential.title'),
|
||||
slug: 'essentials',
|
||||
description: [
|
||||
description:
|
||||
'Manage recurring and one-time billing, including subscriptions and invoices.',
|
||||
features: [
|
||||
intl.get('plan.feature.all_capital_plus'),
|
||||
intl.get('plan.feature.sales_purchases_order'),
|
||||
intl.get('plan.feature.purchase_invoices'),
|
||||
@@ -81,47 +53,35 @@ const getSubscriptionPlans = () => [
|
||||
intl.get('plan.feature.inventory_reports'),
|
||||
intl.get('plan.feature.landed_cost'),
|
||||
],
|
||||
price: '95',
|
||||
periods: [
|
||||
{
|
||||
slug: 'month',
|
||||
label: intl.get('plan.monthly'),
|
||||
price: '95',
|
||||
},
|
||||
{
|
||||
slug: 'year',
|
||||
label: intl.get('plan.yearly'),
|
||||
price: '995',
|
||||
},
|
||||
],
|
||||
currencyCode: 'LYD',
|
||||
},
|
||||
{
|
||||
name: intl.get('plan.capital_enterprise.title'),
|
||||
slug: 'enterprise',
|
||||
description: [
|
||||
intl.get('plan.feature.all_capital_essential'),
|
||||
intl.get('plan.feature.multiply_branches'),
|
||||
intl.get('plan.feature.multiply_warehouses'),
|
||||
intl.get('plan.feature.accounting_dimensions'),
|
||||
intl.get('plan.feature.warehouses_reports'),
|
||||
intl.get('plan.feature.branches_reports'),
|
||||
],
|
||||
price: '120',
|
||||
currencyCode: 'LYD',
|
||||
periods: [
|
||||
{
|
||||
slug: 'month',
|
||||
label: intl.get('plan.monthly'),
|
||||
price: '120',
|
||||
},
|
||||
{
|
||||
slug: 'year',
|
||||
label: intl.get('plan.yearly'),
|
||||
price: '1,195',
|
||||
},
|
||||
],
|
||||
price: '$29',
|
||||
pricePeriod: 'Per Year',
|
||||
},
|
||||
// {
|
||||
// name: intl.get('plan.capital_enterprise.title'),
|
||||
// slug: 'enterprise',
|
||||
// description: [
|
||||
// intl.get('plan.feature.all_capital_essential'),
|
||||
// intl.get('plan.feature.multiply_branches'),
|
||||
// intl.get('plan.feature.multiply_warehouses'),
|
||||
// intl.get('plan.feature.accounting_dimensions'),
|
||||
// intl.get('plan.feature.warehouses_reports'),
|
||||
// intl.get('plan.feature.branches_reports'),
|
||||
// ],
|
||||
// price: '120',
|
||||
// currencyCode: 'LYD',
|
||||
// periods: [
|
||||
// {
|
||||
// slug: 'month',
|
||||
// label: intl.get('plan.monthly'),
|
||||
// price: '120',
|
||||
// },
|
||||
// {
|
||||
// slug: 'year',
|
||||
// label: intl.get('plan.yearly'),
|
||||
// price: '1,195',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
|
||||
const initialState = {
|
||||
@@ -135,9 +95,7 @@ export default createReducer(initialState, {
|
||||
*/
|
||||
[t.INIT_SUBSCRIPTION_PLANS]: (state) => {
|
||||
const plans = getSubscriptionPlans();
|
||||
const periods = getSubscriptionPeriods();
|
||||
|
||||
state.plans = plans;
|
||||
state.periods = periods;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
max-width: 600px;
|
||||
min-width: 600px;
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
@media only screen and (max-width: 1500px) {
|
||||
min-width: 500px;
|
||||
max-width: 500px;
|
||||
}
|
||||
@@ -54,7 +54,7 @@
|
||||
top: 0;
|
||||
width: 600px;
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
@media only screen and (max-width: 1500px) {
|
||||
width: 500px;
|
||||
}
|
||||
@media only screen and (max-width: 1024px) {
|
||||
@@ -99,6 +99,7 @@
|
||||
&__organization {
|
||||
font-size: 16px;
|
||||
opacity: 0.75;
|
||||
margin-top: 2.4rem;
|
||||
|
||||
span>a {
|
||||
text-decoration: underline;
|
||||
@@ -108,17 +109,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
width: 60%;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: auto;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
&__links {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
.setup-subscription-form{
|
||||
margin: 0 auto;
|
||||
padding: 0 80px;
|
||||
margin-top: 40px;
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
|
||||
.billing-plans{
|
||||
max-width: 753px;
|
||||
.paragraph{
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&__section{
|
||||
margin-bottom: 40px;
|
||||
|
||||
.title{
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #6b7382;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.bp4-tab-list {
|
||||
border-bottom: 2px solid #e6e6e6;
|
||||
width: 95%;
|
||||
|
||||
.bp4-tab-indicator-wrapper .bp4-tab-indicator{
|
||||
bottom: -2px;
|
||||
}
|
||||
}
|
||||
.bp4-tab-panel{
|
||||
margin-top: 26px;
|
||||
}
|
||||
.subscribe-button {
|
||||
.bp4-button {
|
||||
background-color: #0063ff;
|
||||
min-height: 41px;
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
.plan-radios,
|
||||
.plan-periods{
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.license-container {
|
||||
|
||||
.bp4-button{
|
||||
margin-top: 14px;
|
||||
padding: 0 30px;
|
||||
}
|
||||
.form-group-license_code{
|
||||
margin-top: 20px;
|
||||
}
|
||||
.bp4-form-content {
|
||||
.bp4-input-group {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.bp4-input {
|
||||
position: relative;
|
||||
width: 59%;
|
||||
height: 41px;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: #444444;
|
||||
}
|
||||
p {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
|
||||
// Plan period radio component.
|
||||
// ---------------------
|
||||
.period-radios{
|
||||
display: flex;
|
||||
}
|
||||
.period-radio{
|
||||
display: inline-flex;
|
||||
background-color: #fcfdff;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 240px;
|
||||
height: 36px;
|
||||
border-radius: 5px;
|
||||
padding: 8px 10px;
|
||||
color: #000;
|
||||
border: 1px solid #dcdcdc;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&.is-selected {
|
||||
border: 1px solid #0069ff;
|
||||
background-color: #fcfdff;
|
||||
}
|
||||
&:not(:first-child) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
&__amount{
|
||||
font-weight: 600;
|
||||
}
|
||||
&__period{
|
||||
color: #2f3863;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&::before {
|
||||
content: '/';
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
|
||||
|
||||
// Plan radio component.
|
||||
// ---------------------
|
||||
.plan-radios{
|
||||
display: flex;
|
||||
}
|
||||
.plan-radio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 215px;
|
||||
min-height: 277px;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
border: 1px solid #dcdcdc;
|
||||
background: #fcfdff;
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-selected {
|
||||
border: 1px solid #0069ff;
|
||||
background-color: #fcfdff;
|
||||
}
|
||||
&:not(:first-child) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&__name {
|
||||
background: #3657ff;
|
||||
border-radius: 3px;
|
||||
padding: 2px 10px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
margin-bottom: 18px;
|
||||
height: 21px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&__description {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
|
||||
li{
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
margin-bottom: 9px;
|
||||
|
||||
&:before{
|
||||
content: '-';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__price {
|
||||
margin-top: auto;
|
||||
font-size: 15px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
&__amount {
|
||||
font-weight: 600;
|
||||
}
|
||||
&__period {
|
||||
font-weight: 400;
|
||||
color: #2f3863;
|
||||
|
||||
&::before {
|
||||
content: '/';
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user