feat: ensure to access dashboard user's subscription is active.

This commit is contained in:
a.bouhuolia
2021-08-30 10:42:39 +02:00
parent 47da64e625
commit 9dca9f3317
59 changed files with 1299 additions and 546 deletions

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import * as Yup from 'yup';
import { useHistory } from 'react-router-dom';
import Toaster from 'components/AppToaster';
import 'style/pages/Setup/PaymentViaVoucherDialog.scss';
@@ -14,7 +15,6 @@ import PaymentViaLicenseForm from './PaymentViaVoucherForm';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
import { Intent } from '@blueprintjs/core';
/**
* Payment via license dialog content.
@@ -35,10 +35,11 @@ function PaymentViaLicenseDialogContent({
const handleSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
// Payment via voucher mutate.
paymentViaVoucherMutate({ ...values })
.then(() => {
Toaster.show({
message: intl.get('payment_has_been_done_successfully'),
message: intl.get('payment_via_voucher.success_message'),
intent: Intent.SUCCESS,
});
return closeDialog('payment-via-voucher');
@@ -54,7 +55,7 @@ function PaymentViaLicenseDialogContent({
}) => {
if (errors.find((e) => e.type === 'LICENSE.CODE.IS.INVALID')) {
setErrors({
license_code: 'The license code is not valid, please try agin.',
license_code: 'payment_via_voucher.license_code_not_valid',
});
}
},

View File

@@ -31,7 +31,7 @@ function PaymentViaLicenseForm({
<Form>
<div className={CLASSES.DIALOG_BODY}>
<p>
<T id={'Pleasse enter your voucher number that you received from reseller.'} />
<T id={'payment_via_voucher.dialog.description'} />
</p>
<FastField name="license_code">

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { useHistory } from 'react-router-dom';
import AppToaster from 'components/AppToaster';
import withGlobalErrors from './withGlobalErrors';
@@ -17,29 +19,31 @@ function GlobalErrors({
globalErrorsSet,
}) {
if (globalErrors.something_wrong) {
toastKeySessionExpired = AppToaster.show({
message: intl.get('ops_something_went_wrong'),
intent: Intent.DANGER,
onDismiss: () => {
globalErrorsSet({ something_wrong: false });
}
}, toastKeySessionExpired);
toastKeySessionExpired = AppToaster.show(
{
message: intl.get('ops_something_went_wrong'),
intent: Intent.DANGER,
onDismiss: () => {
globalErrorsSet({ something_wrong: false });
},
},
toastKeySessionExpired,
);
}
if (globalErrors.session_expired) {
toastKeySomethingWrong = AppToaster.show({
message: intl.get('session_expired'),
intent: Intent.DANGER,
onDismiss: () => {
globalErrorsSet({ session_expired: false });
}
}, toastKeySomethingWrong);
toastKeySomethingWrong = AppToaster.show(
{
message: intl.get('session_expired'),
intent: Intent.DANGER,
onDismiss: () => {
globalErrorsSet({ session_expired: false });
},
},
toastKeySomethingWrong,
);
}
return null;
}
export default compose(
withGlobalErrors,
withGlobalErrorsActions,
)(GlobalErrors);
export default compose(withGlobalErrors, withGlobalErrorsActions)(GlobalErrors);

View File

@@ -1,13 +1,15 @@
import React, { useCallback } from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import WorkflowIcon from './WorkflowIcon';
import { FormattedMessage as T } from 'components';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import { compose } from 'utils';
import 'style/pages/Setup/Congrats.scss';
import { compose } from 'utils';
/**
* Setup congrats page.
@@ -28,15 +30,15 @@ function SetupCongratsPage({ setOrganizationSetupCompleted }) {
<div class="setup-congrats__text">
<h1>
<T id={'congrats_you_are_ready_to_go'} />
<T id={'setup.congrats.title'} />
</h1>
<p class="paragraph">
<T id={'it_is_a_long_established_fact_that_a_reader'} />
<T id={'setup.congrats.description'} />
</p>
<Button intent={Intent.PRIMARY} type="submit" onClick={handleBtnClick}>
<T id={'go_to_dashboard'} />
<T id={'setup.congrats.go_to_dashboard'} />
</Button>
</div>
</div>

View File

@@ -15,7 +15,7 @@ export default function SetupInitializingForm() {
isError,
} = useBuildTenant();
useEffect(() => {
React.useEffect(() => {
buildTenantMutate();
}, [buildTenantMutate]);
@@ -27,32 +27,32 @@ export default function SetupInitializingForm() {
{isLoading ? (
<>
<h1>
<T id={'it_s_time_to_make_your_accounting_really_simple'} />
<T id={'setup.initializing.title'} />
</h1>
<p className={'paragraph'}>
<T
id={
'while_we_set_up_your_account_please_remember_to_verify_your_account'
}
/>
<T id={'setup.initializing.description'} />
</p>
</>
) : isError ? (
<>
<h1>
<T id={'something_went_wrong'} />
<T id={'setup.initializing.something_went_wrong'} />
</h1>
<p class="paragraph">
<T id={'please_refresh_the_page'} />
<T id={'setup.initializing.please_refresh_the_page'} />
</p>
</>
) : (
<>
<h1>
<T id={'waiting_to_redirect'} />
<T id={'setup.initializing.waiting_to_redirect'} />
</h1>
<p class="paragraph">
<T id={'refresh_the_page_if_redirect_not_worked'} />
<T
id={
'setup.initializing.refresh_the_page_if_redirect_not_worked'
}
/>
</p>
</>
)}

View File

@@ -3,11 +3,14 @@ import { Icon, For } from 'components';
import { FormattedMessage as T } from 'components';
import { getFooterLinks } from 'config/footerLinks';
import { useAuthActions, useAuthOrganizationId } from 'hooks/state';
import { useAuthActions } from 'hooks/state';
/**
* Footer item link.
*/
function FooterLinkItem({ title, link }) {
return (
<div class="">
<div class="content__links-item">
<a href={link} target="_blank">
{title}
</a>
@@ -16,20 +19,65 @@ function FooterLinkItem({ title, link }) {
}
/**
* Wizard setup left section.
* Setup left section footer.
*/
export default function SetupLeftSection() {
const { setLogout } = useAuthActions();
const organizationId = useAuthOrganizationId();
function SetupLeftSectionFooter() {
// Retrieve the footer links.
const footerLinks = getFooterLinks();
return (
<div className={'content__footer'}>
<div className={'content__contact-info'}>
<p>
<T id={'setup.left_side.footer_help'} />{' '}
<span>{'+21892-738-1987'}</span>
</p>
</div>
<div className={'content__links'}>
<For render={FooterLinkItem} of={footerLinks} />
</div>
</div>
);
}
/**
* Setup left section header.
*/
function SetupLeftSectionHeader() {
const { setLogout } = useAuthActions();
// Handle logout link click.
const onClickLogout = () => {
setLogout();
};
return (
<div className={'content__header'}>
<h1 className={'content__title'}>
<T id={'setup.left_side.title'} />
</h1>
<p className={'content__text'}>
<T id={'setup.left_side.description'} />
</p>
<div class="content__divider"></div>
<div className={'content__organization'}>
<span class="signout">
<a onClick={onClickLogout} href="#">
<T id={'sign_out'} />
</a>
</span>
</div>
</div>
);
}
/**
* Wizard setup left section.
*/
export default function SetupLeftSection() {
return (
<section className={'setup-page__left-section'}>
<div className={'content'}>
@@ -41,40 +89,8 @@ export default function SetupLeftSection() {
width={190}
/>
</div>
<h1 className={'content__title'}>
<T id={'register_a_new_organization_now'} />
</h1>
<p className={'content__text'}>
<T id={'you_have_a_bigcapital_account'} />
</p>
<span class="content__divider"></span>
<div className={'content__organization'}>
<span class="organization-id">
<T id={'organization_id'} />:{' '}
<span class="id">{organizationId}</span>,
</span>
<br />
<span class="signout">
<a onClick={onClickLogout} href="#">
<T id={'sign_out'} />
</a>
</span>
</div>
<div className={'content__footer'}>
<div className={'content__contact-info'}>
<p>
<T id={'we_re_here_to_help'} /> <span>{'+21892-738-1987'}</span>
</p>
</div>
<div className={'content__links'}>
<For render={FooterLinkItem} of={footerLinks} />
</div>
</div>
<SetupLeftSectionHeader />
<SetupLeftSectionFooter />
</div>
</section>
);

View File

@@ -0,0 +1,15 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
// Retrieve the setup organization form validation.
export const getSetupOrganizationValidation = () =>
Yup.object().shape({
organization_name: Yup.string()
.required()
.label(intl.get('organization_name_')),
financialDateStart: Yup.date().required().label(intl.get('date_start_')),
baseCurrency: Yup.string().required().label(intl.get('base_currency_')),
language: Yup.string().required().label(intl.get('language')),
fiscalYear: Yup.string().required().label(intl.get('fiscal_year_')),
timeZone: Yup.string().required().label(intl.get('time_zone_')),
});

View File

@@ -14,20 +14,18 @@ import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone';
import { FormattedMessage as T } from 'components';
import { FieldRequiredHint, Col, Row, ListSelect } from 'components';
import { Col, Row, ListSelect } from 'components';
import {
momentFormatter,
tansformDateValue,
inputIntent,
handleDateChange
handleDateChange,
} from 'utils';
import { getFiscalYear } from 'common/fiscalYearOptions';
import { getLanguages } from 'common/languagesOptions';
import { getCurrencies } from 'common/currencies';
/**
* Setup organization form.
*/
@@ -46,22 +44,24 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
<FastField name={'organization_name'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'legal_organization_name'} />}
className={'form-group--name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'organization_name'} />}
>
<InputGroup {...field} />
<InputGroup {...field} intent={inputIntent({ error, touched })} />
</FormGroup>
)}
</FastField>
{/* ---------- Financial starting date ---------- */}
<FastField name={'financialDateStart'}>
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
{({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'financial_starting_date'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="financialDateStart" />}
@@ -74,6 +74,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
onChange={handleDateChange((formattedDate) => {
setFieldValue('financialDateStart', formattedDate);
})}
intent={inputIntent({ error, touched })}
/>
</FormGroup>
)}
@@ -89,7 +90,6 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'base_currency'} />}
className={classNames(
'form-group--base-currency',
@@ -101,7 +101,9 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
>
<ListSelect
items={Currencies}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
noResults={
<MenuItem disabled={true} text={<T id={'no_results'} />} />
}
popoverProps={{ minimal: true }}
onItemSelect={(item) => {
setFieldValue('baseCurrency', item.code);
@@ -110,6 +112,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
textProp={'name'}
defaultText={<T id={'select_base_currency'} />}
selectedItem={value}
intent={inputIntent({ error, touched })}
/>
</FormGroup>
)}
@@ -136,7 +139,9 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
>
<ListSelect
items={Languages}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
noResults={
<MenuItem disabled={true} text={<T id={'no_results'} />} />
}
onItemSelect={(item) => {
setFieldValue('language', item.value);
}}
@@ -146,6 +151,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
defaultText={<T id={'select_language'} />}
popoverProps={{ minimal: true }}
filterable={false}
intent={inputIntent({ error, touched })}
/>
</FormGroup>
)}
@@ -154,9 +160,12 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
</Row>
{/* --------- Fiscal Year ----------- */}
<FastField name={'fiscalYear'}>
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
{({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'fiscal_year'} />}
className={classNames(
'form-group--fiscal_year',
@@ -168,14 +177,16 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
>
<ListSelect
items={FiscalYear}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
noResults={
<MenuItem disabled={true} text={<T id={'no_results'} />} />
}
selectedItem={value}
selectedItemProp={'value'}
textProp={'name'}
defaultText={<T id={'select_fiscal_year'} />}
popoverProps={{ minimal: true }}
onItemSelect={(item) => {
setFieldValue('fiscalYear', item.value)
setFieldValue('fiscalYear', item.value);
}}
filterable={false}
/>
@@ -191,7 +202,6 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'time_zone'} />}
className={classNames(
'form-group--time-zone',
@@ -216,20 +226,11 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
</FastField>
<p className={'register-org-note'}>
<T
id={
'note_you_can_change_your_preferences_later_in_dashboard_if_needed'
}
/>
<T id={'setup.organization.note_you_can_change_your_preferences'} />
</p>
<div className={'register-org-button'}>
<Button
intent={Intent.PRIMARY}
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
<Button intent={Intent.PRIMARY} loading={isSubmitting} type="submit">
<T id={'save_continue'} />
</Button>
</div>

View File

@@ -1,9 +1,8 @@
import React from 'react';
import * as Yup from 'yup';
import { Formik } from 'formik';
import moment from 'moment';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import 'style/pages/Setup/Organization.scss';
@@ -13,53 +12,30 @@ import { useOrganizationSetup } from 'hooks/query';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import {
compose,
transfromToSnakeCase,
} from 'utils';
import { compose, transfromToSnakeCase } from 'utils';
import { getSetupOrganizationValidation } from './SetupOrganization.schema';
// Initial values.
const defaultValues = {
organization_name: '',
financialDateStart: moment(new Date()).format('YYYY-MM-DD'),
baseCurrency: '',
language: 'en',
fiscalYear: '',
timeZone: '',
};
/**
* Setup organization form.
*/
function SetupOrganizationPage({
wizard,
setOrganizationSetupCompleted,
}) {
function SetupOrganizationPage({ wizard, setOrganizationSetupCompleted }) {
const { mutateAsync: organizationSetupMutate } = useOrganizationSetup();
// Validation schema.
const validationSchema = Yup.object().shape({
organization_name: Yup.string()
.required()
.label(intl.get('organization_name_')),
financialDateStart: Yup.date()
.required()
.label(intl.get('date_start_')),
baseCurrency: Yup.string()
.required()
.label(intl.get('base_currency_')),
language: Yup.string()
.required()
.label(intl.get('language')),
fiscalYear: Yup.string()
.required()
.label(intl.get('fiscal_year_')),
timeZone: Yup.string()
.required()
.label(intl.get('time_zone_')),
});
// Initial values.
const defaultValues = {
organization_name: '',
financialDateStart: moment(new Date()).format('YYYY-MM-DD'),
baseCurrency: '',
language: 'en',
fiscalYear: '',
timeZone: '',
};
const validationSchema = getSetupOrganizationValidation();
// Initialize values.
const initialValues = {
...defaultValues,
};
@@ -83,10 +59,10 @@ function SetupOrganizationPage({
<div className={'setup-organization'}>
<div className={'setup-organization__title-wrap'}>
<h1>
<T id={'let_s_get_started'} />
<T id={'setup.organization.title'} />
</h1>
<p class="paragraph">
<T id={'tell_the_system_a_little_bit_about_your_organization'} />
<T id={'setup.organization.description'} />
</p>
</div>

View File

@@ -4,7 +4,7 @@ import * as R from 'ramda';
import 'style/pages/Setup/Subscription.scss';
import SetupSubscriptionForm from './SetupSubscriptionForm';
import SetupSubscriptionForm from './SetupSubscription/SetupSubscriptionForm';
import { getSubscriptionFormSchema } from './SubscriptionForm.schema';
import withSubscriptionPlansActions from '../Subscriptions/withSubscriptionPlansActions';
@@ -13,13 +13,11 @@ import withSubscriptionPlansActions from '../Subscriptions/withSubscriptionPlans
*/
function SetupSubscription({
// #withSubscriptionPlansActions
initSubscriptionPlans
initSubscriptionPlans,
}) {
React.useEffect(() => {
initSubscriptionPlans();
}, [
initSubscriptionPlans
]);
}, [initSubscriptionPlans]);
// Initial values.
const initialValues = {
@@ -30,10 +28,14 @@ function SetupSubscription({
// Handle form submit.
const handleSubmit = () => {};
const SubscriptionFormSchema = getSubscriptionFormSchema();
// Retrieve momerized subscription form schema.
const SubscriptionFormSchema = React.useMemo(
() => getSubscriptionFormSchema(),
[],
);
return (
<div className={'setup-subscription-form'}>
<div className={'setup-subscription-form'}>
<Formik
validationSchema={SubscriptionFormSchema}
initialValues={initialValues}
@@ -44,6 +46,4 @@ function SetupSubscription({
);
}
export default R.compose(
withSubscriptionPlansActions,
)(SetupSubscription);
export default R.compose(withSubscriptionPlansActions)(SetupSubscription);

View File

@@ -0,0 +1,16 @@
import React from 'react';
import SubscriptionPlansSection from './SubscriptionPlansSection';
import SubscriptionPeriodsSection from './SubscriptionPeriodsSection';
import SubscriptionPaymentMethodsSection from './SubscriptionPaymentsMethodsSection';
export default function SetupSubscriptionForm() {
return (
<div class="billing-plans">
<SubscriptionPlansSection />
<SubscriptionPeriodsSection />
<SubscriptionPaymentMethodsSection />
</div>
);
}

View File

@@ -0,0 +1,19 @@
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>
);
};

View File

@@ -0,0 +1,43 @@
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 }) => {
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>
);
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { Field } from 'formik';
import { T, SubscriptionPlans } from 'components';
import { compose } from 'utils';
import withPlans from '../../Subscriptions/withPlans';
/**
* Billing plans.
*/
function SubscriptionPlansSection({ 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>
<Field name={'plan_slug'}>
{({ form: { setFieldValue }, field: { value } }) => (
<SubscriptionPlans
value={value}
plans={plans}
onSelect={(value) => {
setFieldValue('plan_slug', value);
}}
/>
)}
</Field>
</section>
);
}
export default compose(withPlans(({ plans }) => ({ plans })))(
SubscriptionPlansSection,
);

View File

@@ -1,15 +0,0 @@
import React from 'react';
import { Form } from 'formik';
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
/**
* Subscription step of wizard setup.
*/
export default function SetupSubscriptionForm() {
return (
<Form>
<BillingPlansForm />
</Form>
);
}

View File

@@ -5,9 +5,7 @@ import { getSetupWizardSteps } from 'common/registerWizard';
function WizardSetupStep({ label, isActive = false }) {
return (
<li className={classNames({ 'is-active': isActive })}>
<p className={'wizard-info'}>
{ label }
</p>
<p className={'wizard-info'}>{label}</p>
</li>
);
}

View File

@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
import * as Yup from 'yup';
import { Formik, Form } from 'formik';
import intl from 'react-intl-universal';
import { If, Alert, T } from 'components';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import 'style/pages/Billing/BillingPage.scss';
@@ -10,8 +11,11 @@ import { MasterBillingTabs } from './SubscriptionTabs';
import withBillingActions from './withBillingActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSubscriptionPlansActions from './withSubscriptionPlansActions';
import { compose } from 'utils';
import { getBillingFormValidationSchema } from './utils';
import withSubscriptions from './withSubscriptions';
/**
* Billing form.
@@ -20,26 +24,30 @@ function BillingForm({
// #withDashboardActions
changePageTitle,
//#withBillingActions
// #withBillingActions
requestSubmitBilling,
initSubscriptionPlans,
// #withSubscriptions
isSubscriptionInactive,
}) {
useEffect(() => {
changePageTitle(intl.get('billing'));
}, [changePageTitle]);
const validationSchema = Yup.object().shape({
plan_slug: Yup.string()
.required(),
period: Yup.string().required(),
license_code: Yup.string().trim(),
});
React.useEffect(() => {
initSubscriptionPlans();
}, [initSubscriptionPlans]);
// Initial values.
const initialValues = {
plan_slug: 'free',
period: 'month',
license_code: '',
};
// Handle form submitting.
const handleSubmit = (values, { setSubmitting }) => {
requestSubmitBilling(values)
.then((response) => {
@@ -53,20 +61,34 @@ function BillingForm({
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={validationSchema}
validationSchema={getBillingFormValidationSchema()}
onSubmit={handleSubmit}
initialValues={initialValues}
>
{({ isSubmitting, handleSubmit }) => (
<Form>
<MasterBillingTabs />
</Form>
)}
<Form>
<MasterBillingTabs />
</Form>
</Formik>
</div>
</DashboardInsider>
);
}
export default compose(withDashboardActions, withBillingActions)(BillingForm);
export default compose(
withDashboardActions,
withBillingActions,
withSubscriptionPlansActions,
withSubscriptions(
({ isSubscriptionInactive }) => ({ isSubscriptionInactive }),
'main',
),
)(BillingForm);

View File

@@ -15,12 +15,14 @@ import { saveInvoke } from 'utils';
function BillingPeriod({
// #ownProps
label,
currencyCode,
value,
selectedOption,
onSelected,
period,
// #withPlan
price,
currencyCode,
}) {
const handlePeriodClick = () => {
saveInvoke(onSelected, value);

View File

@@ -1,42 +1,46 @@
import React from 'react';
import { Field } from 'formik';
import BillingPeriod from './BillingPeriod';
import * as R from 'ramda';
import withPlans from './withPlans';
import { T, SubscriptionPeriods } from 'components';
import { compose } from 'utils';
import withPlan from './withPlan';
/**
* Sunscription periods enhanced.
*/
const SubscriptionPeriodsEnhanced = R.compose(
withPlan(({ plan }) => ({ plan })),
)(({ plan, ...restProps }) => {
return <SubscriptionPeriods periods={plan.periods} {...restProps} />;
});
/**
* Billing periods.
*/
function BillingPeriods({ title, description, plansPeriods }) {
export default function BillingPeriods() {
return (
<section class="billing-plans__section">
<h1 class="title">{title}</h1>
<h1 class="title">
<T id={'setup.plans.select_period.title'} />
</h1>
<div class="description">
<p className="paragraph">{description}</p>
<p className="paragraph">
<T id={'setup.plans.select_period.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: { value }, form: { values, setFieldValue } }) => (
<SubscriptionPeriodsEnhanced
selectedPeriod={value}
planSlug={values.plan_slug}
onPeriodSelect={(period) => {
setFieldValue('period', period);
}}
/>
)}
</Field>
</section>
);
}
export default compose(withPlans(({ plansPeriods }) => ({ plansPeriods })))(
BillingPeriods,
);

View File

@@ -3,8 +3,6 @@ import classNames from 'classnames';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import 'style/pages/Subscription/PlanRadio.scss';
import { saveInvoke } from 'utils';
/**
@@ -32,9 +30,7 @@ export default function BillingPlan({
onClick={handlePlanClick}
>
<div className={'plan-radio__header'}>
<div className={'plan-radio__name'}>
{intl.get('plan_radio_name', { name: name })}
</div>
<div className={'plan-radio__name'}>{name}</div>
</div>
<div className={'plan-radio__description'}>

View File

@@ -1,12 +1,10 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import 'style/pages/Subscription/BillingPlans.scss';
import BillingPlansInput from 'containers/Subscriptions/BillingPlansInput';
import BillingPeriodsInput from 'containers/Subscriptions/BillingPeriodsInput';
import BillingPaymentMethod from 'containers/Subscriptions/BillingPaymentMethod';
import BillingPlansInput from './BillingPlansInput';
import BillingPeriodsInput from './BillingPeriodsInput';
import BillingPaymentMethod from './BillingPaymentMethod';
/**
* Billing plans form.
@@ -14,18 +12,9 @@ import BillingPaymentMethod from 'containers/Subscriptions/BillingPaymentMethod'
export default function BillingPlansForm() {
return (
<div class="billing-plans">
<BillingPlansInput
title={intl.get('select_a_plan')}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
<BillingPeriodsInput
title={intl.get('choose_your_billing')}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
<BillingPaymentMethod
title={intl.get('payment_methods')}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
<BillingPlansInput />
<BillingPeriodsInput />
<BillingPaymentMethod />
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { FastField, Field } from 'formik';
import BillingPlan from './BillingPlan';
import { Field } from 'formik';
import { T, SubscriptionPlans } from 'components';
import withPlans from './withPlans';
import { compose } from 'utils';
@@ -11,27 +11,24 @@ import { compose } from 'utils';
function BillingPlans({ plans, title, description, selectedOption }) {
return (
<section class="billing-plans__section">
<h1 class="title">{title}</h1>
<h1 class="title">
<T id={'setup.plans.select_plan.title'} />
</h1>
<div class="description">
<p className="paragraph">{description}</p>
<p className="paragraph">
<T id={'setup.plans.select_plan.description'} />
</p>
</div>
<Field 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>
<SubscriptionPlans
plans={plans}
value={value}
onSelect={(value) => {
setFieldValue('plan_slug', value);
}}
/>
)}
</Field>
</section>

View File

@@ -1,10 +1,6 @@
import React from 'react';
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
import BillingPlansForm from './BillingPlansForm';
export default function BillingTab() {
return (
<div>
<BillingPlansForm />
</div>
);
return (<BillingPlansForm />);
}

View File

@@ -16,11 +16,7 @@ export const MasterBillingTabs = ({ formik }) => {
id={'billing'}
panel={<BillingTab formik={formik} />}
/>
<Tab
title={intl.get('usage')}
id={'usage'}
disabled={true}
/>
<Tab title={intl.get('usage')} id={'usage'} disabled={true} />
</Tabs>
</div>
);
@@ -43,11 +39,7 @@ export const PaymentMethodTabs = ({ formik }) => {
id={'credit_card'}
disabled={true}
/>
<Tab
title={intl.get('paypal')}
id={'paypal'}
disabled={true}
/>
<Tab title={intl.get('paypal')} id={'paypal'} disabled={true} />
</Tabs>
</div>
);

View File

@@ -1,11 +1,13 @@
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">{ title }</h1>
<p className="paragraph">{ description }</p>
<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>

View File

@@ -0,0 +1,8 @@
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(),
});

View File

@@ -1,17 +1,14 @@
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;
};

View File

@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import {
isSubscriptionsInactiveFactory,
isSubscriptionsActiveFactory
} from 'store/subscription/subscription.selectors';
export default (mapState) => {
const isSubscriptionsInactive = isSubscriptionsInactiveFactory();
const isSubscriptionsActive = isSubscriptionsActiveFactory();
const mapStateToProps = (state, props) => {
const mapped = {
isSubscriptionsInactive: isSubscriptionsInactive(state, props),
isSubscriptionsActive: isSubscriptionsActive(state, props),
};
return (mapState) ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};