mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
Merge branch 'develop' into bulk-categorize-bank-transactions
This commit is contained in:
@@ -27,6 +27,7 @@ import ProjectAlerts from '@/containers/Projects/containers/ProjectAlerts';
|
||||
import TaxRatesAlerts from '@/containers/TaxRates/alerts';
|
||||
import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
|
||||
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
|
||||
import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
|
||||
|
||||
export default [
|
||||
...AccountsAlerts,
|
||||
@@ -56,5 +57,6 @@ export default [
|
||||
...ProjectAlerts,
|
||||
...TaxRatesAlerts,
|
||||
...CashflowAlerts,
|
||||
...BankRulesAlerts
|
||||
...BankRulesAlerts,
|
||||
...SubscriptionAlerts
|
||||
];
|
||||
|
||||
@@ -14,14 +14,17 @@ import {
|
||||
Intent,
|
||||
Switch,
|
||||
Tooltip,
|
||||
MenuDivider,
|
||||
} from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
Icon,
|
||||
DashboardActionsBar,
|
||||
DashboardRowsHeightButton,
|
||||
FormattedMessage as T,
|
||||
AppToaster,
|
||||
If,
|
||||
} from '@/components';
|
||||
|
||||
import { CashFlowMenuItems } from './utils';
|
||||
@@ -37,13 +40,14 @@ import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { withBanking } from '../withBanking';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
useDisconnectBankAccount,
|
||||
useUpdateBankAccount,
|
||||
useExcludeUncategorizedTransactions,
|
||||
useUnexcludeUncategorizedTransactions,
|
||||
} from '@/hooks/query/bank-rules';
|
||||
import { withBankingActions } from '../withBankingActions';
|
||||
import { withBanking } from '../withBanking';
|
||||
|
||||
function AccountTransactionsActionsBar({
|
||||
// #withDialogActions
|
||||
@@ -63,15 +67,21 @@ function AccountTransactionsActionsBar({
|
||||
enableMultipleCategorization,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { accountId } = useAccountTransactionsContext();
|
||||
const { accountId, currentAccount } = useAccountTransactionsContext();
|
||||
|
||||
// Refresh cashflow infinity transactions hook.
|
||||
const { refresh } = useRefreshCashflowTransactionsInfinity();
|
||||
|
||||
const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount();
|
||||
const { mutateAsync: updateBankAccount } = useUpdateBankAccount();
|
||||
|
||||
// Retrieves the money in/out buttons options.
|
||||
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
|
||||
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
|
||||
|
||||
const isFeedsActive = !!currentAccount.is_feeds_active;
|
||||
const isSyncingOwner = currentAccount.is_syncing_owner;
|
||||
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('cashflowTransactions', 'tableSize', size);
|
||||
@@ -100,6 +110,39 @@ function AccountTransactionsActionsBar({
|
||||
const handleBankRulesClick = () => {
|
||||
history.push(`/bank-rules?accountId=${accountId}`);
|
||||
};
|
||||
|
||||
// Handles the bank account disconnect click.
|
||||
const handleDisconnectClick = () => {
|
||||
disconnectBankAccount({ bankAccountId: accountId })
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The bank account has been disconnected.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
};
|
||||
// handles the bank update button click.
|
||||
const handleBankUpdateClick = () => {
|
||||
updateBankAccount({ bankAccountId: accountId })
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The transactions of the bank account has been updated.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
};
|
||||
// Handle the refresh button click.
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
@@ -200,6 +243,24 @@ function AccountTransactionsActionsBar({
|
||||
/>
|
||||
<NavbarDivider />
|
||||
|
||||
<If condition={isSyncingOwner}>
|
||||
<Tooltip
|
||||
content={
|
||||
isFeedsActive
|
||||
? 'The bank syncing is active'
|
||||
: 'The bank syncing is disconnected'
|
||||
}
|
||||
minimal={true}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="feed" iconSize={16} />}
|
||||
intent={isFeedsActive ? Intent.SUCCESS : Intent.DANGER}
|
||||
/>
|
||||
</Tooltip>
|
||||
</If>
|
||||
|
||||
{!isEmpty(uncategorizedTransationsIdsSelected) && (
|
||||
<Button
|
||||
icon={<Icon icon="disable" iconSize={16} />}
|
||||
@@ -246,7 +307,15 @@ function AccountTransactionsActionsBar({
|
||||
}}
|
||||
content={
|
||||
<Menu>
|
||||
<If condition={isSyncingOwner && isFeedsActive}>
|
||||
<MenuItem onClick={handleBankUpdateClick} text={'Update'} />
|
||||
<MenuDivider />
|
||||
</If>
|
||||
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
|
||||
|
||||
<If condition={isSyncingOwner && isFeedsActive}>
|
||||
<MenuItem onClick={handleDisconnectClick} text={'Disconnect'} />
|
||||
</If>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -3,15 +3,14 @@ import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { AppToaster } from '@/components';
|
||||
import { CreateQuickPaymentMadeFormSchema } from './QuickPaymentMade.schema';
|
||||
import { useQuickPaymentMadeContext } from './QuickPaymentMadeFormProvider';
|
||||
import QuickPaymentMadeFormContent from './QuickPaymentMadeFormContent';
|
||||
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { defaultPaymentMade, transformErrors } from './utils';
|
||||
import { defaultPaymentMade, transformBillToForm, transformErrors } from './utils';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
@@ -21,31 +20,24 @@ function QuickPaymentMadeForm({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
|
||||
const {
|
||||
bill,
|
||||
dialogName,
|
||||
createPaymentMadeMutate,
|
||||
} = useQuickPaymentMadeContext();
|
||||
const { bill, dialogName, createPaymentMadeMutate } =
|
||||
useQuickPaymentMadeContext();
|
||||
|
||||
// Initial form values
|
||||
// Initial form values.
|
||||
const initialValues = {
|
||||
...defaultPaymentMade,
|
||||
...bill,
|
||||
...transformBillToForm(bill),
|
||||
};
|
||||
|
||||
// Handles the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting, setFieldError }) => {
|
||||
const entries = [values]
|
||||
.filter((entry) => entry.id && entry.payment_amount)
|
||||
.map((entry) => ({
|
||||
bill_id: entry.id,
|
||||
...pick(entry, ['payment_amount']),
|
||||
}));
|
||||
|
||||
const entries = [
|
||||
{
|
||||
payment_amount: values.amount,
|
||||
bill_id: values.bill_id,
|
||||
},
|
||||
];
|
||||
const form = {
|
||||
...values,
|
||||
vendor_id: values?.vendor?.id,
|
||||
...omit(values, ['bill_id']),
|
||||
entries,
|
||||
};
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ function QuickPaymentMadeFormFields({
|
||||
</Col>
|
||||
</Row>
|
||||
{/*------------ Amount Received -----------*/}
|
||||
<FastField name={'payment_amount'}>
|
||||
<FastField name={'amount'}>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
@@ -135,7 +135,7 @@ function QuickPaymentMadeFormFields({
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames('form-group--payment_amount', CLASSES.FILL)}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="payment_amount" />}
|
||||
helperText={<ErrorMessage name="amount" />}
|
||||
>
|
||||
<ControlGroup>
|
||||
<InputPrependText text={values.currency_code} />
|
||||
@@ -144,7 +144,7 @@ function QuickPaymentMadeFormFields({
|
||||
value={value}
|
||||
minimal={true}
|
||||
onChange={(amount) => {
|
||||
setFieldValue('payment_amount', amount);
|
||||
setFieldValue('amount', amount);
|
||||
}}
|
||||
intent={inputIntent({ error, touched })}
|
||||
inputRef={(ref) => (paymentMadeFieldRef.current = ref)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { DialogContent } from '@/components';
|
||||
import {
|
||||
useBill,
|
||||
@@ -11,7 +11,6 @@ import { Features } from '@/constants';
|
||||
import { useFeatureCan } from '@/hooks/state';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
|
||||
const QuickPaymentMadeContext = React.createContext();
|
||||
|
||||
/**
|
||||
@@ -40,13 +39,14 @@ function QuickPaymentMadeFormProvider({ query, billId, dialogName, ...props }) {
|
||||
isSuccess: isBranchesSuccess,
|
||||
} = useBranches(query, { enabled: isBranchFeatureCan });
|
||||
|
||||
const paymentBill = useMemo(
|
||||
() => pick(bill, ['id', 'due_amount', 'vendor_id', 'currency_code']),
|
||||
[bill],
|
||||
);
|
||||
|
||||
// State provider.
|
||||
const provider = {
|
||||
bill: {
|
||||
...pick(bill, ['id', 'due_amount', 'vendor', 'currency_code']),
|
||||
vendor_id: bill?.vendor?.display_name,
|
||||
payment_amount: bill?.due_amount,
|
||||
},
|
||||
bill: paymentBill,
|
||||
accounts,
|
||||
branches,
|
||||
dialogName,
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import intl from 'react-intl-universal';
|
||||
import { first } from 'lodash';
|
||||
import { first, pick } from 'lodash';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { AppToaster } from '@/components';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useQuickPaymentMadeContext } from './QuickPaymentMadeFormProvider';
|
||||
import { PAYMENT_MADE_ERRORS } from '@/containers/Purchases/PaymentMades/constants';
|
||||
|
||||
// Default initial values of payment made.
|
||||
export const defaultPaymentMade = {
|
||||
bill_id: '',
|
||||
vendor_id: '',
|
||||
payment_account_id: '',
|
||||
payment_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
reference: '',
|
||||
payment_number: '',
|
||||
amount: '',
|
||||
// statement: '',
|
||||
exchange_rate: 1,
|
||||
branch_id: '',
|
||||
entries: [{ bill_id: '', payment_amount: '' }],
|
||||
};
|
||||
|
||||
export const transformErrors = (errors, { setFieldError }) => {
|
||||
@@ -58,3 +59,11 @@ export const useSetPrimaryBranchToForm = () => {
|
||||
}
|
||||
}, [isBranchesSuccess, setFieldValue, branches]);
|
||||
};
|
||||
|
||||
export const transformBillToForm = (bill) => {
|
||||
return {
|
||||
...pick(bill, ['vendor_id', 'currency_code']),
|
||||
amount: bill.due_amount,
|
||||
bill_id: bill.id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { pick, defaultTo, omit } from 'lodash';
|
||||
import { defaultTo, omit } from 'lodash';
|
||||
|
||||
import { AppToaster } from '@/components';
|
||||
import { useQuickPaymentReceiveContext } from './QuickPaymentReceiveFormProvider';
|
||||
@@ -12,7 +12,11 @@ import QuickPaymentReceiveFormContent from './QuickPaymentReceiveFormContent';
|
||||
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { defaultInitialValues, transformErrors } from './utils';
|
||||
import {
|
||||
defaultInitialValues,
|
||||
transformErrors,
|
||||
transformInvoiceToForm,
|
||||
} from './utils';
|
||||
import { compose, transactionNumber } from '@/utils';
|
||||
|
||||
/**
|
||||
@@ -26,14 +30,10 @@ function QuickPaymentReceiveForm({
|
||||
paymentReceiveAutoIncrement,
|
||||
paymentReceiveNumberPrefix,
|
||||
paymentReceiveNextNumber,
|
||||
preferredDepositAccount
|
||||
preferredDepositAccount,
|
||||
}) {
|
||||
|
||||
const {
|
||||
dialogName,
|
||||
invoice,
|
||||
createPaymentReceiveMutate,
|
||||
} = useQuickPaymentReceiveContext();
|
||||
const { dialogName, invoice, createPaymentReceiveMutate } =
|
||||
useQuickPaymentReceiveContext();
|
||||
|
||||
// Payment receive number.
|
||||
const nextPaymentNumber = transactionNumber(
|
||||
@@ -48,24 +48,22 @@ function QuickPaymentReceiveForm({
|
||||
payment_receive_no: nextPaymentNumber,
|
||||
}),
|
||||
deposit_account_id: defaultTo(preferredDepositAccount, ''),
|
||||
...invoice,
|
||||
...transformInvoiceToForm(invoice),
|
||||
};
|
||||
|
||||
// Handles the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting, setFieldError }) => {
|
||||
const entries = [values]
|
||||
.filter((entry) => entry.id && entry.payment_amount)
|
||||
.map((entry) => ({
|
||||
invoice_id: entry.id,
|
||||
...pick(entry, ['payment_amount']),
|
||||
}));
|
||||
|
||||
const entries = [
|
||||
{
|
||||
invoice_id: values.invoice_id,
|
||||
payment_amount: values.amount,
|
||||
},
|
||||
];
|
||||
const form = {
|
||||
...omit(values, ['payment_receive_no']),
|
||||
...omit(values, ['payment_receive_no', 'invoice_id']),
|
||||
...(!paymentReceiveAutoIncrement && {
|
||||
payment_receive_no: values.payment_receive_no,
|
||||
}),
|
||||
customer_id: values.customer.id,
|
||||
entries,
|
||||
};
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ function QuickPaymentReceiveFormFields({
|
||||
</Col>
|
||||
</Row>
|
||||
{/*------------ Amount Received -----------*/}
|
||||
<FastField name={'payment_amount'}>
|
||||
<FastField name={'amount'}>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
@@ -139,7 +139,7 @@ function QuickPaymentReceiveFormFields({
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames('form-group--payment_amount', CLASSES.FILL)}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="payment_amount" />}
|
||||
helperText={<ErrorMessage name="amount" />}
|
||||
>
|
||||
<ControlGroup>
|
||||
<InputPrependText text={values.currency_code} />
|
||||
@@ -148,7 +148,7 @@ function QuickPaymentReceiveFormFields({
|
||||
value={value}
|
||||
minimal={true}
|
||||
onChange={(amount) => {
|
||||
setFieldValue('payment_amount', amount);
|
||||
setFieldValue('amount', amount);
|
||||
}}
|
||||
intent={inputIntent({ error, touched })}
|
||||
inputRef={(ref) => (paymentReceiveFieldRef.current = ref)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import React, { useContext, createContext } from 'react';
|
||||
import React, { useContext, createContext, useMemo } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
import { DialogContent } from '@/components';
|
||||
import { Features } from '@/constants';
|
||||
@@ -47,15 +47,16 @@ function QuickPaymentReceiveFormProvider({
|
||||
isSuccess: isBranchesSuccess,
|
||||
} = useBranches(query, { enabled: isBranchFeatureCan });
|
||||
|
||||
const invoicePayment = useMemo(
|
||||
() => pick(invoice, ['id', 'due_amount', 'customer_id', 'currency_code']),
|
||||
[invoice],
|
||||
);
|
||||
|
||||
// State provider.
|
||||
const provider = {
|
||||
accounts,
|
||||
branches,
|
||||
invoice: {
|
||||
...pick(invoice, ['id', 'due_amount', 'customer', 'currency_code']),
|
||||
customer_id: invoice?.customer?.display_name,
|
||||
payment_amount: invoice.due_amount,
|
||||
},
|
||||
invoice: invoicePayment,
|
||||
isAccountsLoading,
|
||||
isSettingsLoading,
|
||||
isBranchesSuccess,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import intl from 'react-intl-universal';
|
||||
import { first } from 'lodash';
|
||||
import { first, pick } from 'lodash';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { AppToaster } from '@/components';
|
||||
|
||||
@@ -10,15 +10,16 @@ import { useFormikContext } from 'formik';
|
||||
import { useQuickPaymentReceiveContext } from './QuickPaymentReceiveFormProvider';
|
||||
|
||||
export const defaultInitialValues = {
|
||||
invoice_id: '',
|
||||
customer_id: '',
|
||||
deposit_account_id: '',
|
||||
payment_receive_no: '',
|
||||
payment_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
amount: '',
|
||||
// statement: '',
|
||||
exchange_rate: 1,
|
||||
branch_id: '',
|
||||
entries: [{ invoice_id: '', payment_amount: '' }],
|
||||
};
|
||||
|
||||
export const transformErrors = (errors, { setFieldError }) => {
|
||||
@@ -44,7 +45,9 @@ export const transformErrors = (errors, { setFieldError }) => {
|
||||
}
|
||||
if (getError('PAYMENT_ACCOUNT_CURRENCY_INVALID')) {
|
||||
AppToaster.show({
|
||||
message: intl.get('payment_Receive.error.payment_account_currency_invalid'),
|
||||
message: intl.get(
|
||||
'payment_Receive.error.payment_account_currency_invalid',
|
||||
),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
@@ -64,3 +67,11 @@ export const useSetPrimaryBranchToForm = () => {
|
||||
}
|
||||
}, [isBranchesSuccess, setFieldValue, branches]);
|
||||
};
|
||||
|
||||
export const transformInvoiceToForm = (invoice) => {
|
||||
return {
|
||||
...pick(invoice, ['customer_id', 'currency_code']),
|
||||
amount: invoice.due_amount,
|
||||
invoice_id: invoice.id,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,28 +1,66 @@
|
||||
import { Group } from '@/components';
|
||||
import { SubscriptionPlan } from './SubscriptionPlan';
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { AppToaster, Group, GroupProps } from '@/components';
|
||||
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
|
||||
import { SubscriptionPlan } from '@/containers/Subscriptions/component/SubscriptionPlan';
|
||||
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
|
||||
import { useSubscriptionPlans } from './hooks';
|
||||
import { withPlans } from '@/containers/Subscriptions/withPlans';
|
||||
import { withSubscriptionPlanMapper } from '@/containers/Subscriptions/component/withSubscriptionPlanMapper';
|
||||
|
||||
export function SubscriptionPlans() {
|
||||
interface SubscriptionPlansProps {
|
||||
wrapProps?: GroupProps;
|
||||
onSubscribe?: (variantId: number) => void;
|
||||
}
|
||||
|
||||
export function SubscriptionPlans({
|
||||
wrapProps,
|
||||
onSubscribe,
|
||||
}: SubscriptionPlansProps) {
|
||||
const subscriptionPlans = useSubscriptionPlans();
|
||||
|
||||
return (
|
||||
<Group spacing={14} noWrap align="stretch">
|
||||
<Group spacing={14} noWrap align="stretch" {...wrapProps}>
|
||||
{subscriptionPlans.map((plan, index) => (
|
||||
<SubscriptionPlan
|
||||
key={index}
|
||||
slug={plan.slug}
|
||||
label={plan.name}
|
||||
description={plan.description}
|
||||
features={plan.features}
|
||||
featured={plan.featured}
|
||||
monthlyPrice={plan.monthlyPrice}
|
||||
monthlyPriceLabel={plan.monthlyPriceLabel}
|
||||
annuallyPrice={plan.annuallyPrice}
|
||||
annuallyPriceLabel={plan.annuallyPriceLabel}
|
||||
monthlyVariantId={plan.monthlyVariantId}
|
||||
annuallyVariantId={plan.annuallyVariantId}
|
||||
/>
|
||||
<SubscriptionPlanMapped key={index} plan={plan} />
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const SubscriptionPlanMapped = R.compose(
|
||||
withSubscriptionPlanMapper,
|
||||
withPlans(({ plansPeriod }) => ({ plansPeriod })),
|
||||
)(({ plansPeriod, monthlyVariantId, annuallyVariantId, ...props }) => {
|
||||
const { mutateAsync: getLemonCheckout, isLoading } =
|
||||
useGetLemonSqueezyCheckout();
|
||||
|
||||
const handleSubscribeBtnClick = () => {
|
||||
const variantId =
|
||||
SubscriptionPlansPeriod.Monthly === plansPeriod
|
||||
? monthlyVariantId
|
||||
: annuallyVariantId;
|
||||
|
||||
getLemonCheckout({ variantId })
|
||||
.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 (
|
||||
<SubscriptionPlan
|
||||
{...props}
|
||||
onSubscribe={handleSubscribeBtnClick}
|
||||
subscribeButtonProps={{
|
||||
loading: isLoading,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
28
packages/webapp/src/containers/Subscriptions/BillingPage.tsx
Normal file
28
packages/webapp/src/containers/Subscriptions/BillingPage.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { BillingPageBoot } from './BillingPageBoot';
|
||||
import { BillingPageContent } from './BillingPageContent';
|
||||
import { DashboardInsider } from '@/components';
|
||||
import { useDashboardMeta } from '@/hooks/query';
|
||||
import withAlertActions from '../Alert/withAlertActions';
|
||||
|
||||
function BillingPageRoot({ openAlert }) {
|
||||
const { data: dashboardMeta } = useDashboardMeta({
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
// In case the edition is not Bigcapital Cloud, redirect to the homepage.
|
||||
if (!dashboardMeta.is_bigcapital_cloud) {
|
||||
return <Redirect to={{ pathname: '/' }} />;
|
||||
}
|
||||
return (
|
||||
<DashboardInsider>
|
||||
<BillingPageBoot>
|
||||
<BillingPageContent />
|
||||
</BillingPageBoot>
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(withAlertActions)(BillingPageRoot);
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { useGetSubscriptions } from '@/hooks/query/subscription';
|
||||
|
||||
interface BillingBootContextValues {
|
||||
isSubscriptionsLoading: boolean;
|
||||
subscriptions: any;
|
||||
}
|
||||
|
||||
const BillingBoot = createContext<BillingBootContextValues>(
|
||||
{} as BillingBootContextValues,
|
||||
);
|
||||
|
||||
interface BillingPageBootProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function BillingPageBoot({ children }: BillingPageBootProps) {
|
||||
const { isLoading: isSubscriptionsLoading, data: subscriptionsRes } =
|
||||
useGetSubscriptions();
|
||||
|
||||
const mainSubscription = subscriptionsRes?.subscriptions?.find(
|
||||
(s) => s.slug === 'main',
|
||||
);
|
||||
|
||||
const value = {
|
||||
isSubscriptionsLoading,
|
||||
subscriptions: subscriptionsRes?.subscriptions,
|
||||
mainSubscription,
|
||||
};
|
||||
return <BillingBoot.Provider value={value}>{children}</BillingBoot.Provider>;
|
||||
}
|
||||
|
||||
export const useBillingPageBoot = () => React.useContext(BillingBoot);
|
||||
@@ -0,0 +1,8 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 32px 40px;
|
||||
min-width: 800px;
|
||||
max-width: 900px;
|
||||
width: 75%;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
import { Box, Group } from '@/components';
|
||||
import { Spinner, Text } from '@blueprintjs/core';
|
||||
import { Subscription } from './BillingSubscription';
|
||||
import { useBillingPageBoot } from './BillingPageBoot';
|
||||
import styles from './BillingPageContent.module.scss';
|
||||
|
||||
export function BillingPageContent() {
|
||||
const { isSubscriptionsLoading, subscriptions } = useBillingPageBoot();
|
||||
|
||||
if (isSubscriptionsLoading || !subscriptions) {
|
||||
return <Spinner size={30} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={styles.root}>
|
||||
<Text>
|
||||
Only pay for what you really need. All plans come with 24/7 customer support.
|
||||
</Text>
|
||||
|
||||
<Group style={{ marginTop: '2rem' }}>
|
||||
<Subscription />
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
.root {
|
||||
width: 450px;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 -8px 0 0px #BFCCD6, rgb(0 8 36 / 9%) 0px 4px 20px -5px;
|
||||
border: 1px solid #C4D2D7;
|
||||
min-height: 420px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title{
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #3D4C58;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.5;
|
||||
color: #394B59;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.period {
|
||||
div + div {
|
||||
&::before{
|
||||
content: " • ";
|
||||
text-align: center;
|
||||
margin-right: 3px;
|
||||
color: #999;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
&:global(.bp4-intent-success){
|
||||
color: #3e703e;
|
||||
}
|
||||
&:global(.bp4-intent-danger){
|
||||
color: #A82A2A;
|
||||
}
|
||||
}
|
||||
.periodStatus{
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
.priceAmount {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.subscribeButton{
|
||||
border-radius: 32px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.actions{
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import clsx from 'classnames';
|
||||
import { includes } from 'lodash';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import { Button, Card, Classes, Intent, Text } from '@blueprintjs/core';
|
||||
import withAlertActions from '../Alert/withAlertActions';
|
||||
import styles from './BillingSubscription.module.scss';
|
||||
import withDrawerActions from '../Drawer/withDrawerActions';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { useBillingPageBoot } from './BillingPageBoot';
|
||||
import { getSubscriptionStatusText } from './_utils';
|
||||
|
||||
function SubscriptionRoot({ openAlert, openDrawer }) {
|
||||
const { mainSubscription } = useBillingPageBoot();
|
||||
|
||||
// Can't continue if the main subscription is not loaded.
|
||||
if (!mainSubscription) {
|
||||
return null;
|
||||
}
|
||||
const handleCancelSubBtnClick = () => {
|
||||
openAlert('cancel-main-subscription');
|
||||
};
|
||||
const handleResumeSubBtnClick = () => {
|
||||
openAlert('resume-main-subscription');
|
||||
};
|
||||
const handleUpdatePaymentMethod = () => {
|
||||
window.LemonSqueezy.Url.Open(
|
||||
mainSubscription.lemonUrls?.updatePaymentMethod,
|
||||
);
|
||||
};
|
||||
// Handle upgrade button click.
|
||||
const handleUpgradeBtnClick = () => {
|
||||
openDrawer(DRAWERS.CHANGE_SUBSCARIPTION_PLAN);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={styles.root}>
|
||||
<Stack spacing={6}>
|
||||
<h1 className={styles.title}>{mainSubscription.planName}</h1>
|
||||
|
||||
<Group
|
||||
spacing={0}
|
||||
className={clsx(styles.period, {
|
||||
[Classes.INTENT_DANGER]: includes(
|
||||
['on_trial', 'inactive'],
|
||||
mainSubscription.status,
|
||||
),
|
||||
[Classes.INTENT_SUCCESS]: includes(
|
||||
['active', 'canceled'],
|
||||
mainSubscription.status,
|
||||
),
|
||||
})}
|
||||
>
|
||||
<Text className={styles.periodStatus}>
|
||||
{mainSubscription.statusFormatted}
|
||||
</Text>
|
||||
|
||||
<SubscriptionStatusText subscription={mainSubscription} />
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Text className={styles.description}>
|
||||
Control your business bookkeeping with automated accounting, to run
|
||||
intelligent reports for faster decision-making.
|
||||
</Text>
|
||||
|
||||
<Stack align="flex-start" spacing={8} className={styles.actions}>
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
inline
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleUpgradeBtnClick}
|
||||
>
|
||||
Upgrade the Plan
|
||||
</Button>
|
||||
|
||||
{mainSubscription.canceled && (
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
inline
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleResumeSubBtnClick}
|
||||
>
|
||||
Resume Subscription
|
||||
</Button>
|
||||
)}
|
||||
{!mainSubscription.canceled && (
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
inline
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleCancelSubBtnClick}
|
||||
>
|
||||
Cancel Subscription
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
inline
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleUpdatePaymentMethod}
|
||||
>
|
||||
Change Payment Method
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Group position={'apart'} style={{ marginTop: 'auto' }}>
|
||||
<Group spacing={4}>
|
||||
<Text className={styles.priceAmount}>
|
||||
{mainSubscription.planPriceFormatted}
|
||||
</Text>
|
||||
|
||||
{mainSubscription.planPeriod && (
|
||||
<Text className={styles.pricePeriod}>
|
||||
{mainSubscription.planPeriod === 'month'
|
||||
? 'mo'
|
||||
: mainSubscription.planPeriod === 'year'
|
||||
? 'yearly'
|
||||
: ''}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Box>
|
||||
{mainSubscription.canceled && (
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleResumeSubBtnClick}
|
||||
className={styles.subscribeButton}
|
||||
>
|
||||
Resume Subscription
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const Subscription = R.compose(
|
||||
withAlertActions,
|
||||
withDrawerActions,
|
||||
)(SubscriptionRoot);
|
||||
|
||||
function SubscriptionStatusText({ subscription }) {
|
||||
const text = getSubscriptionStatusText(subscription);
|
||||
|
||||
if (!text) return null;
|
||||
|
||||
return <Text className={styles.periodText}>{text}</Text>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
17
packages/webapp/src/containers/Subscriptions/_utils.ts
Normal file
17
packages/webapp/src/containers/Subscriptions/_utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// @ts-nocheck
|
||||
export const getSubscriptionStatusText = (subscription) => {
|
||||
if (subscription.status === 'on_trial') {
|
||||
return subscription.onTrial
|
||||
? `Trials ends in ${subscription.trialEndsAtFormatted}`
|
||||
: `Trial ended ${subscription.trialEndsAtFormatted}`;
|
||||
} else if (subscription.status === 'active') {
|
||||
return subscription.endsAtFormatted
|
||||
? `Renews in ${subscription.endsAtFormatted}`
|
||||
: 'Lifetime subscription';
|
||||
} else if (subscription.status === 'canceled') {
|
||||
return subscription.ended
|
||||
? `Expires ${subscription.endsAtFormatted}`
|
||||
: `Expired ${subscription.endsAtFormatted}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||
|
||||
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||
|
||||
import { useCancelMainSubscription } from '@/hooks/query/subscription';
|
||||
|
||||
/**
|
||||
* Cancel Unlocking partial transactions alerts.
|
||||
*/
|
||||
function CancelMainSubscriptionAlert({
|
||||
name,
|
||||
|
||||
// #withAlertStoreConnect
|
||||
isOpen,
|
||||
payload: { module },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
}) {
|
||||
const { mutateAsync: cancelSubscription, isLoading } =
|
||||
useCancelMainSubscription();
|
||||
|
||||
// Handle cancel.
|
||||
const handleCancel = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
// Handle confirm.
|
||||
const handleConfirm = () => {
|
||||
const values = {
|
||||
module: module,
|
||||
};
|
||||
cancelSubscription()
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The subscription has been canceled.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {},
|
||||
)
|
||||
.finally(() => {
|
||||
closeAlert(name);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={'Cancel Subscription'}
|
||||
intent={Intent.DANGER}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
loading={isLoading}
|
||||
>
|
||||
<p>
|
||||
<strong>The subscription for this organization will end.</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It will no longer be accessible to you or any other users. Make sure any
|
||||
data has already been exported.
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
)(CancelMainSubscriptionAlert);
|
||||
@@ -0,0 +1,79 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||
|
||||
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||
import { useResumeMainSubscription } from '@/hooks/query/subscription';
|
||||
|
||||
/**
|
||||
* Resume Unlocking partial transactions alerts.
|
||||
*/
|
||||
function ResumeMainSubscriptionAlert({
|
||||
name,
|
||||
|
||||
// #withAlertStoreConnect
|
||||
isOpen,
|
||||
payload: { module },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
}) {
|
||||
const { mutateAsync: resumeSubscription, isLoading } =
|
||||
useResumeMainSubscription();
|
||||
|
||||
// Handle cancel.
|
||||
const handleCancel = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
// Handle confirm.
|
||||
const handleConfirm = () => {
|
||||
const values = {
|
||||
module: module,
|
||||
};
|
||||
resumeSubscription()
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The subscription has been resumed.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {},
|
||||
)
|
||||
.finally(() => {
|
||||
closeAlert(name);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={'Resume Subscription'}
|
||||
intent={Intent.DANGER}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
loading={isLoading}
|
||||
>
|
||||
<p>
|
||||
<strong>The subscription for this organization will resume.</strong>
|
||||
|
||||
<p>
|
||||
Are you sure want to resume the subscription of this organization?
|
||||
</p>
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
)(ResumeMainSubscriptionAlert);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
const CancelMainSubscriptionAlert = React.lazy(
|
||||
() => import('./CancelMainSubscriptionAlert'),
|
||||
);
|
||||
const ResumeMainSubscriptionAlert = React.lazy(
|
||||
() => import('./ResumeMainSubscriptionAlert'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Subscription alert.
|
||||
*/
|
||||
export const SubscriptionAlerts = [
|
||||
{
|
||||
name: 'cancel-main-subscription',
|
||||
component: CancelMainSubscriptionAlert,
|
||||
},
|
||||
{
|
||||
name: 'resume-main-subscription',
|
||||
component: ResumeMainSubscriptionAlert,
|
||||
},
|
||||
];
|
||||
@@ -1,14 +1,12 @@
|
||||
// @ts-nocheck
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import * as R from 'ramda';
|
||||
import { AppToaster } from '@/components';
|
||||
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
|
||||
import { PricingPlan } from '@/components/PricingPlan/PricingPlan';
|
||||
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
|
||||
import {
|
||||
WithPlansProps,
|
||||
withPlans,
|
||||
} from '@/containers/Subscriptions/withPlans';
|
||||
import { ButtonProps } from '@blueprintjs/core';
|
||||
|
||||
interface SubscriptionPricingFeature {
|
||||
text: string;
|
||||
@@ -27,8 +25,8 @@ interface SubscriptionPricingProps {
|
||||
monthlyPriceLabel: string;
|
||||
annuallyPrice: string;
|
||||
annuallyPriceLabel: string;
|
||||
monthlyVariantId?: string;
|
||||
annuallyVariantId?: string;
|
||||
onSubscribe?: (variantId: number) => void;
|
||||
subscribeButtonProps?: Optional<ButtonProps>;
|
||||
}
|
||||
|
||||
interface SubscriptionPricingCombinedProps
|
||||
@@ -44,32 +42,14 @@ function SubscriptionPlanRoot({
|
||||
monthlyPriceLabel,
|
||||
annuallyPrice,
|
||||
annuallyPriceLabel,
|
||||
monthlyVariantId,
|
||||
annuallyVariantId,
|
||||
onSubscribe,
|
||||
subscribeButtonProps,
|
||||
|
||||
// #withPlans
|
||||
plansPeriod,
|
||||
}: SubscriptionPricingCombinedProps) {
|
||||
const { mutateAsync: getLemonCheckout, isLoading } =
|
||||
useGetLemonSqueezyCheckout();
|
||||
|
||||
const handleClick = () => {
|
||||
const variantId =
|
||||
SubscriptionPlansPeriod.Monthly === plansPeriod
|
||||
? monthlyVariantId
|
||||
: annuallyVariantId;
|
||||
|
||||
getLemonCheckout({ variantId })
|
||||
.then((res) => {
|
||||
const checkoutUrl = res.data.data.attributes.url;
|
||||
window.LemonSqueezy.Url.Open(checkoutUrl);
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong!',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
onSubscribe && onSubscribe();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -85,7 +65,7 @@ function SubscriptionPlanRoot({
|
||||
subPrice={annuallyPriceLabel}
|
||||
/>
|
||||
)}
|
||||
<PricingPlan.BuyButton loading={isLoading} onClick={handleClick}>
|
||||
<PricingPlan.BuyButton onClick={handleClick} {...subscribeButtonProps}>
|
||||
Subscribe
|
||||
</PricingPlan.BuyButton>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
|
||||
interface WithSubscriptionPlanProps {
|
||||
plan: any;
|
||||
onSubscribe?: (variantId: number) => void;
|
||||
}
|
||||
|
||||
interface MappedSubscriptionPlanProps {
|
||||
slug: string;
|
||||
label: string;
|
||||
description: string;
|
||||
features: any[];
|
||||
featured: boolean;
|
||||
monthlyPrice: string;
|
||||
monthlyPriceLabel: string;
|
||||
annuallyPrice: string;
|
||||
annuallyPriceLabel: string;
|
||||
monthlyVariantId: number;
|
||||
annuallyVariantId: number;
|
||||
onSubscribe?: (variantId: number) => void;
|
||||
}
|
||||
|
||||
export const withSubscriptionPlanMapper = <
|
||||
P extends MappedSubscriptionPlanProps,
|
||||
>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
) => {
|
||||
return function WithSubscriptionPlanMapper(
|
||||
props: WithSubscriptionPlanProps &
|
||||
Omit<P, keyof MappedSubscriptionPlanProps>,
|
||||
) {
|
||||
const { plan, onSubscribe, ...restProps } = props;
|
||||
|
||||
const mappedProps: MappedSubscriptionPlanProps = {
|
||||
slug: plan.slug,
|
||||
label: plan.name,
|
||||
description: plan.description,
|
||||
features: plan.features,
|
||||
featured: plan.featured,
|
||||
monthlyPrice: plan.monthlyPrice,
|
||||
monthlyPriceLabel: plan.monthlyPriceLabel,
|
||||
annuallyPrice: plan.annuallyPrice,
|
||||
annuallyPriceLabel: plan.annuallyPriceLabel,
|
||||
monthlyVariantId: plan.monthlyVariantId,
|
||||
annuallyVariantId: plan.annuallyVariantId,
|
||||
onSubscribe,
|
||||
};
|
||||
return <WrappedComponent {...mappedProps} {...(restProps as P)} />;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Callout, Classes } from '@blueprintjs/core';
|
||||
import { Box } from '@/components';
|
||||
import { SubscriptionPlansPeriodSwitcher } from '@/containers/Setup/SetupSubscription/SubscriptionPlansPeriodSwitcher';
|
||||
import { ChangeSubscriptionPlans } from './ChangeSubscriptionPlans';
|
||||
|
||||
export default function ChangeSubscriptionPlanContent() {
|
||||
return (
|
||||
<Box className={Classes.DRAWER_BODY}>
|
||||
<Box
|
||||
style={{
|
||||
maxWidth: 1024,
|
||||
margin: '0 auto',
|
||||
padding: '50px 20px 80px',
|
||||
}}
|
||||
>
|
||||
<Callout style={{ marginBottom: '2rem' }} icon={null}>
|
||||
Simple plans. Simple prices. Only pay for what you really need. All
|
||||
plans come with award-winning 24/7 customer support. Prices do not
|
||||
include applicable taxes.
|
||||
</Callout>
|
||||
|
||||
<SubscriptionPlansPeriodSwitcher />
|
||||
<ChangeSubscriptionPlans />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// @ts-nocheck
|
||||
import React, { lazy } from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Drawer, DrawerHeaderContent, DrawerSuspense } from '@/components';
|
||||
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||
import { Position } from '@blueprintjs/core';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
const ChangeSubscriptionPlanContent = lazy(
|
||||
() => import('./ChangeSubscriptionPlanContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Account drawer.
|
||||
*/
|
||||
function ChangeSubscriptionPlanDrawer({
|
||||
name,
|
||||
// #withDrawer
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
size={'calc(100% - 5px)'}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<DrawerHeaderContent
|
||||
name={DRAWERS.CHANGE_SUBSCARIPTION_PLAN}
|
||||
title={'Change Subscription Plan'}
|
||||
/>
|
||||
<ChangeSubscriptionPlanContent />
|
||||
</DrawerSuspense>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(withDrawers())(ChangeSubscriptionPlanDrawer);
|
||||
@@ -0,0 +1,72 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { AppToaster, Group } from '@/components';
|
||||
import { SubscriptionPlan } from '../../component/SubscriptionPlan';
|
||||
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
|
||||
import { useSubscriptionPlans } from '@/hooks/constants/useSubscriptionPlans';
|
||||
import { useChangeSubscriptionPlan } from '@/hooks/query/subscription';
|
||||
import { withSubscriptionPlanMapper } from '../../component/withSubscriptionPlanMapper';
|
||||
import { withPlans } from '../../withPlans';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
export function ChangeSubscriptionPlans() {
|
||||
const subscriptionPlans = useSubscriptionPlans();
|
||||
|
||||
return (
|
||||
<Group spacing={14} noWrap align="stretch">
|
||||
{subscriptionPlans.map((plan, index) => (
|
||||
<SubscriptionPlanMapped plan={plan} />
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export const SubscriptionPlanMapped = R.compose(
|
||||
withSubscriptionPlanMapper,
|
||||
withDrawerActions,
|
||||
withPlans(({ plansPeriod }) => ({ plansPeriod })),
|
||||
)(
|
||||
({
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
monthlyVariantId,
|
||||
annuallyVariantId,
|
||||
plansPeriod,
|
||||
...props
|
||||
}) => {
|
||||
const { mutateAsync: changeSubscriptionPlan, isLoading } =
|
||||
useChangeSubscriptionPlan();
|
||||
|
||||
// Handles the subscribe button click.
|
||||
const handleSubscribe = () => {
|
||||
const variantId =
|
||||
plansPeriod === SubscriptionPlansPeriod.Monthly
|
||||
? monthlyVariantId
|
||||
: annuallyVariantId;
|
||||
|
||||
changeSubscriptionPlan({ variant_id: variantId })
|
||||
.then(() => {
|
||||
closeDrawer(DRAWERS.CHANGE_SUBSCARIPTION_PLAN);
|
||||
AppToaster.show({
|
||||
message: 'The subscription plan has been changed.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<SubscriptionPlan
|
||||
{...props}
|
||||
onSubscribe={handleSubscribe}
|
||||
subscribeButtonProps={{ loading: isLoading }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * as default from './ChangeSubscriptionPlanDrawer';
|
||||
@@ -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