Merge branch 'develop' into bulk-categorize-bank-transactions

This commit is contained in:
Ahmed Bouhuolia
2024-08-01 13:46:03 +02:00
83 changed files with 2245 additions and 699 deletions

View File

@@ -2,6 +2,6 @@
import { Position, Toaster, Intent } from '@blueprintjs/core';
export const AppToaster = Toaster.create({
position: Position.RIGHT_BOTTOM,
position: Position.TOP,
intent: Intent.WARNING,
});

View File

@@ -15,7 +15,7 @@ import { useAuthActions } from '@/hooks/state';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useAuthenticatedAccount } from '@/hooks/query';
import { useAuthenticatedAccount, useDashboardMeta } from '@/hooks/query';
import { firstLettersArgs, compose } from '@/utils';
/**
@@ -31,6 +31,9 @@ function DashboardTopbarUser({
// Retrieve authenticated user information.
const { data: user } = useAuthenticatedAccount();
const { data: dashboardMeta } = useDashboardMeta({
keepPreviousData: true,
});
const onClickLogout = () => {
setLogout();
};
@@ -58,6 +61,12 @@ function DashboardTopbarUser({
}
/>
<MenuDivider />
{dashboardMeta.is_bigcapital_cloud && (
<MenuItem
text={'Billing'}
onClick={() => history.push('/billing')}
/>
)}
<MenuItem
text={<T id={'keyboard_shortcuts'} />}
onClick={onKeyboardShortcut}
@@ -79,6 +88,4 @@ function DashboardTopbarUser({
</Popover>
);
}
export default compose(
withDialogActions,
)(DashboardTopbarUser);
export default compose(withDialogActions)(DashboardTopbarUser);

View File

@@ -22,6 +22,7 @@ import RefundVendorCreditDetailDrawer from '@/containers/Drawers/RefundVendorCre
import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer';
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
import ChangeSubscriptionPlanDrawer from '@/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer';
import { DRAWERS } from '@/constants/drawers';
@@ -63,6 +64,7 @@ export default function DrawersContainer() {
/>
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
<ChangeSubscriptionPlanDrawer name={DRAWERS.CHANGE_SUBSCARIPTION_PLAN} />
</div>
);
}

View File

@@ -24,4 +24,5 @@ export enum DRAWERS {
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
CATEGORIZE_TRANSACTION = 'categorize-transaction',
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan'
}

View File

@@ -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
];

View File

@@ -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>
}
>

View File

@@ -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,
};

View File

@@ -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)}

View File

@@ -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,

View File

@@ -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,
};
}

View File

@@ -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,
};

View File

@@ -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)}

View File

@@ -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,

View File

@@ -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,
};
};

View File

@@ -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,
}}
/>
);
});

View File

@@ -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);

View 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);

View File

@@ -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);

View File

@@ -0,0 +1,8 @@
.root {
display: flex;
flex-direction: column;
padding: 32px 40px;
min-width: 800px;
max-width: 900px;
width: 75%;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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>;
}

View File

@@ -1,7 +0,0 @@
// @ts-nocheck
import React from 'react';
import BillingPlansForm from './BillingPlansForm';
export default function BillingTab() {
return (<BillingPlansForm />);
}

View File

@@ -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);

View File

@@ -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>
);
};

View 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 '';
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
},
];

View File

@@ -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>

View File

@@ -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)} />;
};
};

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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 }}
/>
);
},
);

View File

@@ -0,0 +1 @@
export * as default from './ChangeSubscriptionPlanDrawer';

View File

@@ -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(),
});

View File

@@ -0,0 +1,5 @@
import { SubscriptionPlans } from '@/constants/subscriptionModels';
export const useSubscriptionPlans = () => {
return SubscriptionPlans;
};

View File

@@ -61,6 +61,76 @@ export function useCreateBankRule(
);
}
interface DisconnectBankAccountRes {}
interface DisconnectBankAccountValues {
bankAccountId: number;
}
/**
* Disconnects the given bank account.
* @param {UseMutationOptions<DisconnectBankAccountRes, Error, DisconnectBankAccountValues>} options
* @returns {UseMutationResult<DisconnectBankAccountRes, Error, DisconnectBankAccountValues>}
*/
export function useDisconnectBankAccount(
options?: UseMutationOptions<
DisconnectBankAccountRes,
Error,
DisconnectBankAccountValues
>,
): UseMutationResult<
DisconnectBankAccountRes,
Error,
DisconnectBankAccountValues
> {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<
DisconnectBankAccountRes,
Error,
DisconnectBankAccountValues
>(
({ bankAccountId }) =>
apiRequest.post(`/banking/bank_accounts/${bankAccountId}/disconnect`),
{
...options,
onSuccess: (res, values) => {
queryClient.invalidateQueries([t.ACCOUNT, values.bankAccountId]);
},
},
);
}
interface UpdateBankAccountRes {}
interface UpdateBankAccountValues {
bankAccountId: number;
}
/**
* Update the bank transactions of the bank account.
* @param {UseMutationOptions<UpdateBankAccountRes, Error, UpdateBankAccountValues>}
* @returns {UseMutationResult<UpdateBankAccountRes, Error, UpdateBankAccountValues>}
*/
export function useUpdateBankAccount(
options?: UseMutationOptions<
UpdateBankAccountRes,
Error,
UpdateBankAccountValues
>,
): UseMutationResult<UpdateBankAccountRes, Error, UpdateBankAccountValues> {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<DisconnectBankAccountRes, Error, UpdateBankAccountValues>(
({ bankAccountId }) =>
apiRequest.post(`/banking/bank_accounts/${bankAccountId}/update`),
{
...options,
onSuccess: () => {},
},
);
}
interface EditBankRuleValues {
id: number;
value: any;

View File

@@ -0,0 +1,196 @@
// @ts-nocheck
import {
useMutation,
UseMutationOptions,
UseMutationResult,
useQuery,
useQueryClient,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import useApiRequest from '../useRequest';
import { transformToCamelCase } from '@/utils';
const QueryKeys = {
Subscriptions: 'Subscriptions',
};
interface CancelMainSubscriptionValues {}
interface CancelMainSubscriptionResponse {}
/**
* Cancels the main subscription of the current organization.
* @param {UseMutationOptions<CreateBankRuleValues, Error, CreateBankRuleValues>} options -
* @returns {UseMutationResult<CreateBankRuleValues, Error, CreateBankRuleValues>}TCHES
*/
export function useCancelMainSubscription(
options?: UseMutationOptions<
CancelMainSubscriptionValues,
Error,
CancelMainSubscriptionResponse
>,
): UseMutationResult<
CancelMainSubscriptionValues,
Error,
CancelMainSubscriptionResponse
> {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<
CancelMainSubscriptionValues,
Error,
CancelMainSubscriptionResponse
>(
(values) =>
apiRequest.post(`/subscription/cancel`, values).then((res) => res.data),
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.Subscriptions);
},
...options,
},
);
}
interface ResumeMainSubscriptionValues {}
interface ResumeMainSubscriptionResponse {}
/**
* Resumes the main subscription of the current organization.
* @param {UseMutationOptions<CreateBankRuleValues, Error, CreateBankRuleValues>} options -
* @returns {UseMutationResult<CreateBankRuleValues, Error, CreateBankRuleValues>}TCHES
*/
export function useResumeMainSubscription(
options?: UseMutationOptions<
ResumeMainSubscriptionValues,
Error,
ResumeMainSubscriptionResponse
>,
): UseMutationResult<
ResumeMainSubscriptionValues,
Error,
ResumeMainSubscriptionResponse
> {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<
ResumeMainSubscriptionValues,
Error,
ResumeMainSubscriptionResponse
>(
(values) =>
apiRequest.post(`/subscription/resume`, values).then((res) => res.data),
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.Subscriptions);
},
...options,
},
);
}
interface ChangeMainSubscriptionPlanValues {
variant_id: string;
}
interface ChangeMainSubscriptionPlanResponse {}
/**
* Changese the main subscription of the current organization.
* @param {UseMutationOptions<ChangeMainSubscriptionPlanValues, Error, ChangeMainSubscriptionPlanResponse>} options -
* @returns {UseMutationResult<ChangeMainSubscriptionPlanValues, Error, ChangeMainSubscriptionPlanResponse>}
*/
export function useChangeSubscriptionPlan(
options?: UseMutationOptions<
ChangeMainSubscriptionPlanValues,
Error,
ChangeMainSubscriptionPlanResponse
>,
): UseMutationResult<
ChangeMainSubscriptionPlanValues,
Error,
ChangeMainSubscriptionPlanResponse
> {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<
ChangeMainSubscriptionPlanResponse,
Error,
ChangeMainSubscriptionPlanValues
>(
(values) =>
apiRequest.post(`/subscription/change`, values).then((res) => res.data),
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.Subscriptions);
},
...options,
},
);
}
interface LemonSubscription {
active: boolean;
canceled: string | null;
canceledAt: string | null;
canceledAtFormatted: string | null;
cancelsAt: string | null;
cancelsAtFormatted: string | null;
createdAt: string;
ended: boolean;
endsAt: string | null;
inactive: boolean;
lemonSubscriptionId: string;
lemon_urls: {
updatePaymentMethod: string;
customerPortal: string;
customerPortalUpdateSubscription: string;
};
onTrial: boolean;
planId: number;
planName: string;
planSlug: string;
slug: string;
startsAt: string | null;
status: string;
statusFormatted: string;
tenantId: number;
trialEndsAt: string | null;
trialEndsAtFormatted: string | null;
trialStartsAt: string | null;
trialStartsAtFormatted: string | null;
updatedAt: string;
}
interface GetSubscriptionsQuery {}
interface GetSubscriptionsResponse {
subscriptions: Array<LemonSubscription>;
}
/**
* Changese the main subscription of the current organization.
* @param {UseMutationOptions<ChangeMainSubscriptionPlanValues, Error, ChangeMainSubscriptionPlanResponse>} options -
* @returns {UseMutationResult<ChangeMainSubscriptionPlanValues, Error, ChangeMainSubscriptionPlanResponse>}
*/
export function useGetSubscriptions(
options?: UseQueryOptions<
GetSubscriptionsQuery,
Error,
GetSubscriptionsResponse
>,
): UseQueryResult<GetSubscriptionsResponse, Error> {
const apiRequest = useApiRequest();
return useQuery<GetSubscriptionsQuery, Error, GetSubscriptionsResponse>(
[QueryKeys.Subscriptions],
(values) =>
apiRequest
.get(`/subscription`)
.then((res) => transformToCamelCase(res.data)),
{
...options,
},
);
}

View File

@@ -1231,6 +1231,13 @@ export const getDashboardRoutes = () => [
breadcrumb: 'Bank Rules',
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: '/billing',
component: lazy(() => import('@/containers/Subscriptions/BillingPage')),
pageTitle: 'Billing',
breadcrumb: 'Billing',
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Homepage
{
path: `/`,

View File

@@ -635,4 +635,11 @@ export default {
],
viewBox: '0 0 16 16',
},
feed: {
path: [
'M1.99,11.99c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S3.1,11.99,1.99,11.99zM2.99,7.99c-0.55,0-1,0.45-1,1s0.45,1,1,1c1.66,0,3,1.34,3,3c0,0.55,0.45,1,1,1s1-0.45,1-1C7.99,10.23,5.75,7.99,2.99,7.99zM2.99,3.99c-0.55,0-1,0.45-1,1s0.45,1,1,1c3.87,0,7,3.13,7,7c0,0.55,0.45,1,1,1s1-0.45,1-1C11.99,8.02,7.96,3.99,2.99,3.99zM2.99-0.01c-0.55,0-1,0.45-1,1s0.45,1,1,1c6.08,0,11,4.92,11,11c0,0.55,0.45,1,1,1s1-0.45,1-1C15.99,5.81,10.17-0.01,2.99-0.01z',
],
viewBox: '0 0 16 16',
},
};