feat: billing page in dashboard and setup.

This commit is contained in:
a.bouhuolia
2021-01-31 13:16:01 +02:00
parent 732c3bbfd7
commit e093be0663
60 changed files with 1505 additions and 1073 deletions

View File

@@ -1,13 +1,21 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { Button, Intent } from '@blueprintjs/core';
import { Formik, Form } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { MeteredBillingTabs, PaymentMethodTabs } from './SubscriptionTabs';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import 'style/pages/Billing/BillingPage.scss';
import { MasterBillingTabs } from './SubscriptionTabs';
import withBillingActions from './withBillingActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/**
* Billing form.
*/
function BillingForm({
// #withDashboardActions
changePageTitle,
@@ -23,51 +31,43 @@ function BillingForm({
const validationSchema = Yup.object().shape({
plan_slug: Yup.string()
.required()
.label(formatMessage({ id: 'plan_slug' })),
.required(),
period: Yup.string().required(),
license_code: Yup.string().trim(),
});
const initialValues = useMemo(
() => ({
plan_slug: 'free',
license_code: '',
}),
[],
);
const initialValues = {
plan_slug: 'free',
period: 'month',
license_code: '',
};
const formik = useFormik({
enableReinitialize: true,
validationSchema: validationSchema,
initialValues: {
...initialValues,
},
onSubmit: (values, { setSubmitting, resetForm, setErrors }) => {
requestSubmitBilling(values)
.then((response) => {
setSubmitting(false);
})
.catch((errors) => {
setSubmitting(false);
});
},
});
const handleSubmit = (values, { setSubmitting }) => {
requestSubmitBilling(values)
.then((response) => {
setSubmitting(false);
})
.catch((errors) => {
setSubmitting(false);
});
};
return (
<div className={'billing-form'}>
<form onSubmit={formik.handleSubmit}>
<MeteredBillingTabs formik={formik} planId={formik.values.plan_slug} />
<div className={'subscribe-button'}>
<Button
intent={Intent.PRIMARY}
type="submit"
loading={formik.isSubmitting}
>
<T id={'subscribe'} />
</Button>
</div>
</form>
</div>
<DashboardInsider name={'billing-page'}>
<div className={'billing-page'}>
<Formik
validationSchema={validationSchema}
onSubmit={handleSubmit}
initialValues={initialValues}
>
{({ isSubmitting, handleSubmit }) => (
<Form>
<MasterBillingTabs />
</Form>
)}
</Formik>
</div>
</DashboardInsider>
);
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import classNames from 'classnames';
import { get } from 'lodash';
import { compose } from 'redux';
import 'style/pages/Subscription/PlanPeriodRadio.scss';
import withPlan from 'containers/Subscriptions/withPlan';
import { saveInvoke } from 'utils';
/**
* Billing period.
*/
function BillingPeriod({
// #ownProps
label,
currencyCode,
value,
selectedOption,
onSelected,
period,
price,
}) {
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);

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { Field } from 'formik';
import BillingPeriod from './BillingPeriod';
import withPlans from './withPlans';
import { compose } from 'utils';
/**
* Billing periods.
*/
function BillingPeriods({ title, description, plansPeriods }) {
return (
<section class="billing-plans__section">
<h1 class="title">{title}</h1>
<div class="description">
<p className="paragraph">{description}</p>
</div>
<Field name={'period'}>
{({ form: { setFieldValue, values } }) => (
<div className={'plan-periods'}>
{plansPeriods.map((period) => (
<BillingPeriod
planSlug={values.plan_slug}
period={period.slug}
label={period.label}
value={period.slug}
onSelected={(value) => setFieldValue('period', value)}
selectedOption={values.period}
/>
))}
</div>
)}
</Field>
</section>
);
}
export default compose(withPlans(({ plansPeriods }) => ({ plansPeriods })))(
BillingPeriods,
);

View File

@@ -0,0 +1,57 @@
import React from 'react';
import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
import 'style/pages/Subscription/PlanRadio.scss';
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'}>
<T id={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>
);
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { FormattedMessage as T } from 'react-intl';
import 'style/pages/Subscription/BillingPlans.scss'
import BillingPlansInput from 'containers/Subscriptions/BillingPlansInput';
import BillingPeriodsInput from 'containers/Subscriptions/BillingPeriodsInput';
import BillingPaymentMethod from 'containers/Subscriptions/BillingPaymentMethod';
/**
* Billing plans form.
*/
export default function BillingPlansForm() {
return (
<div class="billing-plans">
<BillingPlansInput
title={<T id={'select_a_plan'} values={{ order: 1 }} />}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
<BillingPeriodsInput
title={<T id={'choose_your_billing'} values={{ order: 2 }} />}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
<BillingPaymentMethod
title={<T id={'payment_methods'} values={{ order: 3 }} />}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { FastField } from 'formik';
import BillingPlan from './BillingPlan';
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">{title}</h1>
<div class="description">
<p className="paragraph">{description}</p>
</div>
<FastField name={'plan_slug'}>
{({ form: { setFieldValue }, field: { value } }) => (
<div className={'plan-radios'}>
{plans.map((plan) => (
<BillingPlan
name={plan.name}
description={plan.description}
slug={plan.slug}
price={plan.price.month}
currencyCode={plan.currencyCode}
value={plan.slug}
onSelected={(value) => setFieldValue('plan_slug', value)}
selectedOption={value}
/>
))}
</div>
)}
</FastField>
</section>
);
}
export default compose(withPlans(({ plans }) => ({ plans })))(BillingPlans);

View File

@@ -1,16 +1,10 @@
import React from 'react';
import BillingPlans from 'containers/Subscriptions/billingPlans';
import BillingPeriods from 'containers/Subscriptions/billingPeriods';
import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod';
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
function BillingTab({ formik }) {
export default function BillingTab() {
return (
<div>
<BillingPlans title={'a_select_a_plan'} formik={formik} />
<BillingPeriods title={'b_choose_your_billing'} formik={formik} />
<BillingPaymentmethod title={'c_payment_methods'} formik={formik} />
<BillingPlansForm />
</div>
);
}
export default BillingTab;
}

View File

@@ -1,35 +1,38 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { InputGroup, FormGroup, Intent } from '@blueprintjs/core';
import ErrorMessage from 'components/ErrorMessage';
import { FormattedMessage as T } from 'react-intl';
import { Intent, Button } from '@blueprintjs/core';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'redux';
import { useFormikContext } from 'formik';
/**
* Payment via license code tab.
*/
function LicenseTab({ openDialog }) {
const { submitForm, values } = useFormikContext();
const handleSubmitBtnClick = () => {
submitForm().then(() => {
openDialog('payment-via-voucher', { ...values });
});
};
function LicenseTab({
formik: { errors, touched, setFieldValue, getFieldProps },
}) {
return (
<div className={'license-container'}>
<h4>
<T id={'license_code'} />
</h4>
<h3>
<T id={'voucher'} />
</h3>
<p className="paragraph">
<T id={'cards_will_be_charged'} />
</p>
<FormGroup
label={<T id={'license_number'} />}
intent={errors.license_code && touched.license_code && Intent.DANGER}
helperText={
<ErrorMessage name="license_code" {...{ errors, touched }} />
}
className={'form-group-license_code'}
>
<InputGroup
intent={errors.license_code && touched.license_code && Intent.DANGER}
{...getFieldProps('license_code')}
/>
</FormGroup>
<Button onClick={handleSubmitBtnClick} intent={Intent.PRIMARY} large={true}>
Submit Voucher
</Button>
</div>
);
}
export default LicenseTab;
export default compose(withDialogActions)(LicenseTab);

View File

@@ -1,16 +1,18 @@
import React, { useState } from 'react';
import React from 'react';
import { Tabs, Tab } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import BillingTab from './BillingTab';
import LicenseTab from './LicenseTab';
export const MeteredBillingTabs = ({ formik }) => {
const [animate, setAnimate] = useState(true);
/**
* Master billing tabs.
*/
export const MasterBillingTabs = ({ formik }) => {
const { formatMessage } = useIntl();
return (
<div>
<Tabs animate={animate} large={true}>
<Tabs animate={true} large={true}>
<Tab
title={formatMessage({ id: 'billing' })}
id={'billing'}
@@ -20,23 +22,24 @@ export const MeteredBillingTabs = ({ formik }) => {
title={formatMessage({ id: 'usage' })}
id={'usage'}
disabled={true}
// panel={'Usage'}
/>
</Tabs>
</div>
);
};
/**
* Payment methods tabs.
*/
export const PaymentMethodTabs = ({ formik }) => {
const [animate, setAnimate] = useState(true);
const { formatMessage } = useIntl();
return (
<div>
<Tabs animate={animate} large={true}>
<Tabs animate={true} large={true}>
<Tab
title={formatMessage({ id: 'license' })}
id={'license'}
title={formatMessage({ id: 'voucher' })}
id={'voucher'}
panel={<LicenseTab formik={formik} />}
/>
<Tab

View File

@@ -1,16 +1,12 @@
import React, { useState, useRef, useEffect } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import React from 'react';
import { PaymentMethodTabs } from './SubscriptionTabs';
export const BillingPaymentmethod = ({ formik, title }) => {
export default ({ formik, title, description }) => {
return (
<section class="billing-section">
<h1 className={'bg-title'}>
<T id={title} />
</h1>
<p className='paragraph'>
<T id={'please_enter_your_preferred_payment_method'} />
</p>
<section class="billing-plans__section">
<h1 className="title">{ title }</h1>
<p className="paragraph">{ description }</p>
<PaymentMethodTabs formik={formik} />
</section>
);

View File

@@ -1,69 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import classNames from 'classnames';
import { paymentmethod } from 'common/subscriptionModels';
function BillingPeriod({ price, period, currency, onSelected, selected }) {
return (
<a
href={'#!'}
id={'monthly'}
className={`period-container ${classNames({
'billing-selected': selected,
})} `}
>
<span className={'bg-period'}>
<T id={period} />
</span>
<div className={'plan-price'}>
<span className={'amount'}>
{price} {currency}
</span>
<span className={'period'}>
<T id={'year'} />
</span>
</div>
</a>
);
}
function BillingPeriods({ formik, title, selected = 1 }) {
const billingRef = useRef(null);
useEffect(() => {
const billingPriod = billingRef.current.querySelectorAll('a');
const billingSelected = billingRef.current.querySelector(
'.billing-selected',
);
billingPriod.forEach((el) => {
el.addEventListener('click', () => {
billingSelected.classList.remove('billing-selected');
el.classList.add('billing-selected');
});
});
});
return (
<section class="billing-section">
<h1>
<T id={title} />
</h1>
<p className='paragraph'>
<T id={'please_enter_your_preferred_payment_method'} />
</p>
<div className={'payment-method-continer'} ref={billingRef}>
{paymentmethod.map((pay, index) => (
<BillingPeriod
period={pay.period}
price={pay.price}
currency={pay.currency}
onSelected={()=>formik.setFieldValue('period', pay.period)}
selected={selected == index + 1}
/>
))}
</div>
</section>
);
}
export default BillingPeriods;

View File

@@ -1,89 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import classNames from 'classnames';
import { plans } from 'common/subscriptionModels';
function BillingPlan({
name,
description,
price,
slug,
currency,
onSelected,
selected,
}) {
return (
<a
id={'basic-plan'}
className={`plan-wrapper ${classNames({
'plan-selected': selected,
})} `}
onClick={() => onSelected(slug)}
>
<div className={'plan-header'}>
<div className={'plan-name'}>
<T id={name} />
</div>
</div>
<div className={'plan-description'}>
<ul>
{description.map((desc, index) => (
<li>{desc}</li>
))}
</ul>
</div>
<div className={'plan-price'}>
<span className={'amount'}>
{' '}
{price} {currency}
</span>
<span className={'period'}>
<T id={'year_per'} />
</span>
</div>
</a>
);
}
function BillingPlans({ formik, title, selected = 1 }) {
const planRef = useRef(null);
useEffect(() => {
const plans = planRef.current.querySelectorAll('a');
const planSelected = planRef.current.querySelector('.plan-selected');
plans.forEach((el) => {
el.addEventListener('click', () => {
planSelected.classList.remove('plan-selected');
el.classList.add('plan-selected');
});
});
});
return (
<section class="billing-section">
<h1>
<T id={title} />
</h1>
<p className='paragraph'>
<T id={'please_enter_your_preferred_payment_method'} />
</p>
<div className={'billing-form__plan-container'} ref={planRef}>
{plans.map((plan, index) => (
<BillingPlan
name={plan.name}
description={plan.description}
slug={plan.slug}
price={plan.price}
currency={plan.currency}
onSelected={() => formik.setFieldValue('plan_slug', plan.slug)}
selected={selected == index + 1}
/>
))}
</div>
</section>
);
}
export default BillingPlans;

View File

@@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import { getPlanSelector } from 'store/plans/plans.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const getPlan = getPlanSelector();
const mapped = {
plan: getPlan(state, props),
};
return (mapState) ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import {
getPlansSelector,
getPlansPeriodsSelector,
} from 'store/plans/plans.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const getPlans = getPlansSelector();
const getPlansPeriods = getPlansPeriodsSelector();
const mapped = {
plans: getPlans(state, props),
plansPeriods: getPlansPeriods(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};