mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
feat: billing subscription page
This commit is contained in:
@@ -1,95 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { Formik, Form } from 'formik';
|
|
||||||
import { DashboardInsider, If, Alert, T } from '@/components';
|
|
||||||
|
|
||||||
import '@/style/pages/Billing/BillingPage.scss';
|
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
import { MasterBillingTabs } from './SubscriptionTabs';
|
|
||||||
import { getBillingFormValidationSchema } from './utils';
|
|
||||||
|
|
||||||
import withBillingActions from './withBillingActions';
|
|
||||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
|
||||||
import withSubscriptionPlansActions from './withSubscriptionPlansActions';
|
|
||||||
import withSubscriptions from './withSubscriptions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Billing form.
|
|
||||||
*/
|
|
||||||
function BillingForm({
|
|
||||||
// #withDashboardActions
|
|
||||||
changePageTitle,
|
|
||||||
|
|
||||||
// #withBillingActions
|
|
||||||
requestSubmitBilling,
|
|
||||||
|
|
||||||
initSubscriptionPlans,
|
|
||||||
|
|
||||||
// #withSubscriptions
|
|
||||||
isSubscriptionInactive,
|
|
||||||
}) {
|
|
||||||
useEffect(() => {
|
|
||||||
changePageTitle(intl.get('billing'));
|
|
||||||
}, [changePageTitle]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
initSubscriptionPlans();
|
|
||||||
}, [initSubscriptionPlans]);
|
|
||||||
|
|
||||||
// Initial values.
|
|
||||||
const initialValues = {
|
|
||||||
plan_slug: 'essentials',
|
|
||||||
period: 'month',
|
|
||||||
license_code: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle form submitting.
|
|
||||||
const handleSubmit = (values, { setSubmitting }) => {
|
|
||||||
requestSubmitBilling({
|
|
||||||
...values,
|
|
||||||
plan_slug: 'essentials-monthly',
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
setSubmitting(false);
|
|
||||||
})
|
|
||||||
.catch((errors) => {
|
|
||||||
setSubmitting(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardInsider name={'billing-page'}>
|
|
||||||
<div className={'billing-page'}>
|
|
||||||
<If condition={isSubscriptionInactive}>
|
|
||||||
<Alert
|
|
||||||
intent={'danger'}
|
|
||||||
title={<T id={'billing.suspend_message.title'} />}
|
|
||||||
description={<T id={'billing.suspend_message.description'} />}
|
|
||||||
/>
|
|
||||||
</If>
|
|
||||||
|
|
||||||
<Formik
|
|
||||||
validationSchema={getBillingFormValidationSchema()}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
initialValues={initialValues}
|
|
||||||
>
|
|
||||||
<Form>
|
|
||||||
<MasterBillingTabs />
|
|
||||||
</Form>
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
</DashboardInsider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(
|
|
||||||
withDashboardActions,
|
|
||||||
withBillingActions,
|
|
||||||
withSubscriptionPlansActions,
|
|
||||||
withSubscriptions(
|
|
||||||
({ isSubscriptionInactive }) => ({ isSubscriptionInactive }),
|
|
||||||
'main',
|
|
||||||
),
|
|
||||||
)(BillingForm);
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { T } from '@/components';
|
|
||||||
import { PaymentMethodTabs } from './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,59 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { get } from 'lodash';
|
|
||||||
|
|
||||||
|
|
||||||
import '@/style/pages/Subscription/PlanPeriodRadio.scss';
|
|
||||||
|
|
||||||
import withPlan from '@/containers/Subscriptions/withPlan';
|
|
||||||
|
|
||||||
import { saveInvoke, compose } from '@/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Billing period.
|
|
||||||
*/
|
|
||||||
function BillingPeriod({
|
|
||||||
// #ownProps
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
selectedOption,
|
|
||||||
onSelected,
|
|
||||||
period,
|
|
||||||
|
|
||||||
// #withPlan
|
|
||||||
price,
|
|
||||||
currencyCode,
|
|
||||||
}) {
|
|
||||||
const handlePeriodClick = () => {
|
|
||||||
saveInvoke(onSelected, value);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id={`plan-period-${period}`}
|
|
||||||
className={classNames(
|
|
||||||
{
|
|
||||||
'is-selected': value === selectedOption,
|
|
||||||
},
|
|
||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(
|
|
||||||
withPlan(({ plan }, state, { period }) => ({
|
|
||||||
price: get(plan, `price.${period}`),
|
|
||||||
currencyCode: get(plan, 'currencyCode'),
|
|
||||||
})),
|
|
||||||
)(BillingPeriod);
|
|
||||||
@@ -1,49 +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 './withPlan';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sunscription periods enhanced.
|
|
||||||
*/
|
|
||||||
const SubscriptionPeriodsEnhanced = R.compose(
|
|
||||||
withPlan(({ plan }) => ({ plan })),
|
|
||||||
)(({ plan, ...restProps }) => {
|
|
||||||
if (!plan) return null;
|
|
||||||
|
|
||||||
return <SubscriptionPeriods periods={plan.periods} {...restProps} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Billing periods.
|
|
||||||
*/
|
|
||||||
export default function BillingPeriods() {
|
|
||||||
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'}>
|
|
||||||
{({ field: { value }, form: { values, setFieldValue } }) => (
|
|
||||||
<SubscriptionPeriodsEnhanced
|
|
||||||
selectedPeriod={value}
|
|
||||||
planSlug={values.plan_slug}
|
|
||||||
onPeriodSelect={(period) => {
|
|
||||||
setFieldValue('period', period);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
|
|
||||||
import { saveInvoke } from '@/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Billing plan.
|
|
||||||
*/
|
|
||||||
export default function BillingPlan({
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import * as R from 'ramda';
|
|
||||||
|
|
||||||
import '@/style/pages/Subscription/BillingPlans.scss';
|
|
||||||
|
|
||||||
import BillingPlansInput from './BillingPlansInput';
|
|
||||||
import BillingPeriodsInput from './BillingPeriodsInput';
|
|
||||||
import BillingPaymentMethod from './BillingPaymentMethod';
|
|
||||||
|
|
||||||
import withSubscriptions from './withSubscriptions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Billing plans form.
|
|
||||||
*/
|
|
||||||
export default function BillingPlansForm() {
|
|
||||||
return (
|
|
||||||
<div class="billing-plans">
|
|
||||||
<BillingPlansInput />
|
|
||||||
<BillingPeriodsInput />
|
|
||||||
<BillingPaymentMethodWhenSubscriptionInactive />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Billing payment methods when subscription is inactive.
|
|
||||||
* @returns {JSX.Element}
|
|
||||||
*/
|
|
||||||
function BillingPaymentMethodWhenSubscriptionInactiveJSX({
|
|
||||||
// # withSubscriptions
|
|
||||||
isSubscriptionActive,
|
|
||||||
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return !isSubscriptionActive ? <BillingPaymentMethod {...props} /> : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BillingPaymentMethodWhenSubscriptionInactive = R.compose(
|
|
||||||
withSubscriptions(({ isSubscriptionActive }) => ({ isSubscriptionActive })),
|
|
||||||
)(BillingPaymentMethodWhenSubscriptionInactiveJSX);
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { Field } from 'formik';
|
|
||||||
import { T, SubscriptionPlans } from '@/components';
|
|
||||||
|
|
||||||
import withPlans from './withPlans';
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Billing plans.
|
|
||||||
*/
|
|
||||||
function BillingPlans({ plans, title, description, selectedOption }) {
|
|
||||||
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>
|
|
||||||
|
|
||||||
<Field name={'plan_slug'}>
|
|
||||||
{({ form: { setFieldValue }, field: { value } }) => (
|
|
||||||
<SubscriptionPlans
|
|
||||||
plans={plans}
|
|
||||||
value={value}
|
|
||||||
onSelect={(value) => {
|
|
||||||
setFieldValue('plan_slug', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default compose(withPlans(({ plans }) => ({ plans })))(BillingPlans);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import BillingPlansForm from './BillingPlansForm';
|
|
||||||
|
|
||||||
export default function BillingTab() {
|
|
||||||
return (<BillingPlansForm />);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { Intent, Button } from '@blueprintjs/core';
|
|
||||||
import { useFormikContext } from 'formik';
|
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payment via license code tab.
|
|
||||||
*/
|
|
||||||
function LicenseTab({ openDialog }) {
|
|
||||||
const { submitForm, values } = useFormikContext();
|
|
||||||
|
|
||||||
const handleSubmitBtnClick = () => {
|
|
||||||
submitForm().then(() => {
|
|
||||||
openDialog('payment-via-voucher', { ...values });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'license-container'}>
|
|
||||||
<h3>
|
|
||||||
<T id={'voucher'} />
|
|
||||||
</h3>
|
|
||||||
<p className="paragraph">
|
|
||||||
<T id={'cards_will_be_charged'} />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmitBtnClick}
|
|
||||||
intent={Intent.PRIMARY}
|
|
||||||
large={true}
|
|
||||||
>
|
|
||||||
<T id={'submit_voucher'} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(withDialogActions)(LicenseTab);
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { Tabs, Tab } from '@blueprintjs/core';
|
|
||||||
import BillingTab from './BillingTab';
|
|
||||||
import LicenseTab from './LicenseTab';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Master billing tabs.
|
|
||||||
*/
|
|
||||||
export const MasterBillingTabs = ({ formik }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Tabs animate={true} large={true}>
|
|
||||||
<Tab
|
|
||||||
title={intl.get('billing')}
|
|
||||||
id={'billing'}
|
|
||||||
panel={<BillingTab formik={formik} />}
|
|
||||||
/>
|
|
||||||
<Tab title={intl.get('usage')} id={'usage'} disabled={true} />
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payment methods tabs.
|
|
||||||
*/
|
|
||||||
export const PaymentMethodTabs = ({ formik }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Tabs animate={true} large={true}>
|
|
||||||
<Tab
|
|
||||||
title={intl.get('voucher')}
|
|
||||||
id={'voucher'}
|
|
||||||
panel={<LicenseTab formik={formik} />}
|
|
||||||
/>
|
|
||||||
<Tab
|
|
||||||
title={intl.get('credit_card')}
|
|
||||||
id={'credit_card'}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
<Tab title={intl.get('paypal')} id={'paypal'} disabled={true} />
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const getBillingFormValidationSchema = () =>
|
|
||||||
Yup.object().shape({
|
|
||||||
plan_slug: Yup.string().required(),
|
|
||||||
period: Yup.string().required(),
|
|
||||||
license_code: Yup.string().trim(),
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user