mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +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;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: ${(props: GroupProps) => (props.align || 'center')};
|
||||||
flex-wrap: ${(props: GroupProps) => (props.noWrap ? 'nowrap' : 'wrap')};
|
flex-wrap: ${(props: GroupProps) => (props.noWrap ? 'nowrap' : 'wrap')};
|
||||||
justify-content: ${(props: GroupProps) =>
|
justify-content: ${(props: GroupProps) =>
|
||||||
GROUP_POSITIONS[props.position || 'left']};
|
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 './Details';
|
||||||
export * from './TotalLines/index';
|
export * from './TotalLines/index';
|
||||||
export * from './Alert';
|
export * from './Alert';
|
||||||
export * from './Subscriptions';
|
|
||||||
export * from './Dashboard';
|
export * from './Dashboard';
|
||||||
export * from './Drawer';
|
export * from './Drawer';
|
||||||
export * from './Forms';
|
export * from './Forms';
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ function SetupLeftSectionHeader() {
|
|||||||
<p className={'content__text'}>
|
<p className={'content__text'}>
|
||||||
<T id={'setup.left_side.description'} />
|
<T id={'setup.left_side.description'} />
|
||||||
</p>
|
</p>
|
||||||
<div class="content__divider"></div>
|
|
||||||
|
|
||||||
<div className={'content__organization'}>
|
<div className={'content__organization'}>
|
||||||
<span class="signout">
|
<span class="signout">
|
||||||
|
|||||||
@@ -29,10 +29,7 @@ function SetupRightSection({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className={'setup-page__right-section'}>
|
<section className={'setup-page__right-section'}>
|
||||||
<SetupWizardContent
|
<SetupWizardContent stepId={setupStepId} stepIndex={setupStepIndex} />
|
||||||
setupStepId={setupStepId}
|
|
||||||
setupStepIndex={setupStepIndex}
|
|
||||||
/>
|
|
||||||
<SetupDialogs />
|
<SetupDialogs />
|
||||||
</section>
|
</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
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import { T } from '@/components';
|
||||||
import { Field } from 'formik';
|
|
||||||
|
|
||||||
import { T, SubscriptionPlans } from '@/components';
|
import { SubscriptionPlans } from './SubscriptionPlan';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
import withPlans from '../../Subscriptions/withPlans';
|
import withPlans from '../../Subscriptions/withPlans';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Billing plans.
|
* Billing plans.
|
||||||
*/
|
*/
|
||||||
function SubscriptionPlansSection({ plans }) {
|
function SubscriptionPlansSectionRoot({ plans }) {
|
||||||
return (
|
return (
|
||||||
<section class="billing-plans__section">
|
<section>
|
||||||
<h1 class="title">
|
<p className="paragraph" style={{ marginBottom: '1.2rem' }}>
|
||||||
<T id={'setup.plans.select_plan.title'} />
|
<T id={'setup.plans.select_plan.description'} />
|
||||||
</h1>
|
</p>
|
||||||
<div class="description">
|
|
||||||
<p className="paragraph">
|
|
||||||
<T id={'setup.plans.select_plan.description'} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field name={'plan_slug'}>
|
<SubscriptionPlans plans={plans} />
|
||||||
{({ form: { setFieldValue }, field: { value } }) => (
|
|
||||||
<SubscriptionPlans
|
|
||||||
value={value}
|
|
||||||
plans={plans}
|
|
||||||
onSelect={(value) => {
|
|
||||||
setFieldValue('plan_slug', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(withPlans(({ plans }) => ({ plans })))(
|
export const SubscriptionPlansSection = compose(
|
||||||
SubscriptionPlansSection,
|
withPlans(({ plans }) => ({ plans })),
|
||||||
);
|
)(SubscriptionPlansSectionRoot);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.items {
|
||||||
|
padding: 40px 40px 20px;
|
||||||
|
}
|
||||||
@@ -1,30 +1,50 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import SetupSteps from './SetupSteps';
|
import SetupSubscription from './SetupSubscription/SetupSubscription';
|
||||||
import WizardSetupSteps from './WizardSetupSteps';
|
|
||||||
|
|
||||||
import SetupSubscription from './SetupSubscription';
|
|
||||||
import SetupOrganizationPage from './SetupOrganizationPage';
|
import SetupOrganizationPage from './SetupOrganizationPage';
|
||||||
import SetupInitializingForm from './SetupInitializingForm';
|
import SetupInitializingForm from './SetupInitializingForm';
|
||||||
import SetupCongratsPage from './SetupCongratsPage';
|
import SetupCongratsPage from './SetupCongratsPage';
|
||||||
|
import { Stepper } from '@/components/Stepper';
|
||||||
|
import styles from './SetupWizardContent.module.scss';
|
||||||
|
|
||||||
|
interface SetupWizardContentProps {
|
||||||
|
stepIndex: number;
|
||||||
|
stepId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup wizard content.
|
* Setup wizard content.
|
||||||
*/
|
*/
|
||||||
export default function SetupWizardContent({ setupStepIndex, setupStepId }) {
|
export default function SetupWizardContent({
|
||||||
|
stepIndex,
|
||||||
|
stepId,
|
||||||
|
}: SetupWizardContentProps) {
|
||||||
return (
|
return (
|
||||||
<div class="setup-page__content">
|
<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">
|
<Stepper.Step label={'Organization'}>
|
||||||
<SetupSteps step={{ id: setupStepId }}>
|
|
||||||
<SetupSubscription id="subscription" />
|
|
||||||
<SetupOrganizationPage id="organization" />
|
<SetupOrganizationPage id="organization" />
|
||||||
|
</Stepper.Step>
|
||||||
|
|
||||||
|
<Stepper.Step label={'Initiializing'}>
|
||||||
<SetupInitializingForm id={'initializing'} />
|
<SetupInitializingForm id={'initializing'} />
|
||||||
|
</Stepper.Step>
|
||||||
|
|
||||||
|
<Stepper.Step label={'Congrats'}>
|
||||||
<SetupCongratsPage id="congrats" />
|
<SetupCongratsPage id="congrats" />
|
||||||
</SetupSteps>
|
</Stepper.Step>
|
||||||
</div>
|
</Stepper>
|
||||||
</div>
|
</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 = {}) => {
|
export const useGetLemonSqueezyCheckout = (props = {}) => {
|
||||||
const apiRequest = useApiRequest();
|
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],
|
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.
|
// Payment modes.
|
||||||
{
|
{
|
||||||
path: `/payment-mades/import`,
|
path: `/payment-mades/import`,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default (mapState) => {
|
|||||||
const mapped = {
|
const mapped = {
|
||||||
...condits,
|
...condits,
|
||||||
setupStepId: setupStep?.step,
|
setupStepId: setupStep?.step,
|
||||||
setupStepIndex: scenarios.indexOf(setupStep) + 1,
|
setupStepIndex: scenarios.indexOf(setupStep),
|
||||||
};
|
};
|
||||||
return mapState ? mapState(mapped, state, props) : mapped;
|
return mapState ? mapState(mapped, state, props) : mapped;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,22 +3,13 @@ import { createReducer } from '@reduxjs/toolkit';
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import t from '@/store/types';
|
import t from '@/store/types';
|
||||||
|
|
||||||
const getSubscriptionPeriods = () => [
|
|
||||||
{
|
|
||||||
slug: 'month',
|
|
||||||
label: intl.get('plan.monthly'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'year',
|
|
||||||
label: intl.get('plan.yearly'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getSubscriptionPlans = () => [
|
const getSubscriptionPlans = () => [
|
||||||
{
|
{
|
||||||
name: intl.get('plan.capital_basic.title'),
|
name: intl.get('plan.capital_basic.title'),
|
||||||
slug: 'capital_basic',
|
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_invoices'),
|
||||||
intl.get('plan.feature.sales_estimates'),
|
intl.get('plan.feature.sales_estimates'),
|
||||||
intl.get('plan.feature.customers'),
|
intl.get('plan.feature.customers'),
|
||||||
@@ -27,25 +18,15 @@ const getSubscriptionPlans = () => [
|
|||||||
intl.get('plan.feature.expenses_tracking'),
|
intl.get('plan.feature.expenses_tracking'),
|
||||||
intl.get('plan.feature.basic_financial_reports'),
|
intl.get('plan.feature.basic_financial_reports'),
|
||||||
],
|
],
|
||||||
price: '55',
|
price: '$29',
|
||||||
periods: [
|
pricePeriod: 'Per Year',
|
||||||
{
|
|
||||||
slug: 'month',
|
|
||||||
label: intl.get('plan.monthly'),
|
|
||||||
price: '55',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'year',
|
|
||||||
label: intl.get('plan.yearly'),
|
|
||||||
price: '595',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
currencyCode: 'LYD',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: intl.get('plan.capital_plus.title'),
|
name: intl.get('plan.capital_plus.title'),
|
||||||
slug: 'capital_plus',
|
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.all_capital_basic'),
|
||||||
intl.get('plan.feature.predefined_user_roles'),
|
intl.get('plan.feature.predefined_user_roles'),
|
||||||
intl.get('plan.feature.custom_tables_views'),
|
intl.get('plan.feature.custom_tables_views'),
|
||||||
@@ -53,25 +34,16 @@ const getSubscriptionPlans = () => [
|
|||||||
intl.get('plan.feature.plus_financial_reports'),
|
intl.get('plan.feature.plus_financial_reports'),
|
||||||
intl.get('plan.feature.custom_fields_resources'),
|
intl.get('plan.feature.custom_fields_resources'),
|
||||||
],
|
],
|
||||||
price: '75',
|
price: '$29',
|
||||||
periods: [
|
pricePeriod: 'Per Year',
|
||||||
{
|
featured: true,
|
||||||
slug: 'month',
|
|
||||||
label: intl.get('plan.monthly'),
|
|
||||||
price: '75',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'year',
|
|
||||||
label: intl.get('plan.yearly'),
|
|
||||||
price: '795',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
currencyCode: 'LYD',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: intl.get('plan.essential.title'),
|
name: intl.get('plan.essential.title'),
|
||||||
slug: 'essentials',
|
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.all_capital_plus'),
|
||||||
intl.get('plan.feature.sales_purchases_order'),
|
intl.get('plan.feature.sales_purchases_order'),
|
||||||
intl.get('plan.feature.purchase_invoices'),
|
intl.get('plan.feature.purchase_invoices'),
|
||||||
@@ -81,47 +53,35 @@ const getSubscriptionPlans = () => [
|
|||||||
intl.get('plan.feature.inventory_reports'),
|
intl.get('plan.feature.inventory_reports'),
|
||||||
intl.get('plan.feature.landed_cost'),
|
intl.get('plan.feature.landed_cost'),
|
||||||
],
|
],
|
||||||
price: '95',
|
price: '$29',
|
||||||
periods: [
|
pricePeriod: 'Per Year',
|
||||||
{
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// 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 = {
|
const initialState = {
|
||||||
@@ -135,9 +95,7 @@ export default createReducer(initialState, {
|
|||||||
*/
|
*/
|
||||||
[t.INIT_SUBSCRIPTION_PLANS]: (state) => {
|
[t.INIT_SUBSCRIPTION_PLANS]: (state) => {
|
||||||
const plans = getSubscriptionPlans();
|
const plans = getSubscriptionPlans();
|
||||||
const periods = getSubscriptionPeriods();
|
|
||||||
|
|
||||||
state.plans = plans;
|
state.plans = plans;
|
||||||
state.periods = periods;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1500px) {
|
||||||
min-width: 500px;
|
min-width: 500px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1500px) {
|
||||||
width: 500px;
|
width: 500px;
|
||||||
}
|
}
|
||||||
@media only screen and (max-width: 1024px) {
|
@media only screen and (max-width: 1024px) {
|
||||||
@@ -99,6 +99,7 @@
|
|||||||
&__organization {
|
&__organization {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
margin-top: 2.4rem;
|
||||||
|
|
||||||
span>a {
|
span>a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@@ -108,17 +109,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__divider {
|
|
||||||
height: 1px;
|
|
||||||
width: 60%;
|
|
||||||
background: rgba(255, 255, 255, 0.25);
|
|
||||||
margin: 18px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__links {
|
&__links {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
.setup-subscription-form{
|
.setup-subscription-form{
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 80px;
|
padding: 0 40px;
|
||||||
margin-top: 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