+
Subscribe
diff --git a/packages/webapp/src/containers/Subscriptions/component/withSubscriptionPlanMapper.tsx b/packages/webapp/src/containers/Subscriptions/component/withSubscriptionPlanMapper.tsx
new file mode 100644
index 000000000..b0cd58938
--- /dev/null
+++ b/packages/webapp/src/containers/Subscriptions/component/withSubscriptionPlanMapper.tsx
@@ -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,
+) => {
+ return function WithSubscriptionPlanMapper(
+ props: WithSubscriptionPlanProps &
+ Omit
,
+ ) {
+ 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 ;
+ };
+};
diff --git a/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanContent.tsx b/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanContent.tsx
new file mode 100644
index 000000000..85d0361e8
--- /dev/null
+++ b/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanContent.tsx
@@ -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 (
+
+
+
+ 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.
+
+
+
+
+
+
+ );
+}
diff --git a/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer.tsx b/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer.tsx
new file mode 100644
index 000000000..a8aaf60ad
--- /dev/null
+++ b/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer.tsx
@@ -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 (
+
+
+
+
+
+
+ );
+}
+
+export default R.compose(withDrawers())(ChangeSubscriptionPlanDrawer);
diff --git a/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlans.tsx b/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlans.tsx
new file mode 100644
index 000000000..1edc6d7cf
--- /dev/null
+++ b/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlans.tsx
@@ -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 (
+
+ {subscriptionPlans.map((plan, index) => (
+
+ ))}
+
+ );
+}
+
+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 (
+
+ );
+ },
+);
diff --git a/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/index.ts b/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/index.ts
new file mode 100644
index 000000000..4af1d02b2
--- /dev/null
+++ b/packages/webapp/src/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/index.ts
@@ -0,0 +1 @@
+export * as default from './ChangeSubscriptionPlanDrawer';
\ No newline at end of file
diff --git a/packages/webapp/src/containers/Subscriptions/utils.tsx b/packages/webapp/src/containers/Subscriptions/utils.tsx
deleted file mode 100644
index 041234fc9..000000000
--- a/packages/webapp/src/containers/Subscriptions/utils.tsx
+++ /dev/null
@@ -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(),
- });
diff --git a/packages/webapp/src/hooks/constants/useSubscriptionPlans.tsx b/packages/webapp/src/hooks/constants/useSubscriptionPlans.tsx
new file mode 100644
index 000000000..3beb2a34b
--- /dev/null
+++ b/packages/webapp/src/hooks/constants/useSubscriptionPlans.tsx
@@ -0,0 +1,5 @@
+import { SubscriptionPlans } from '@/constants/subscriptionModels';
+
+export const useSubscriptionPlans = () => {
+ return SubscriptionPlans;
+};
diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts
index e6098a37e..1774d2ea8 100644
--- a/packages/webapp/src/hooks/query/bank-rules.ts
+++ b/packages/webapp/src/hooks/query/bank-rules.ts
@@ -61,6 +61,76 @@ export function useCreateBankRule(
);
}
+interface DisconnectBankAccountRes {}
+interface DisconnectBankAccountValues {
+ bankAccountId: number;
+}
+
+/**
+ * Disconnects the given bank account.
+ * @param {UseMutationOptions} options
+ * @returns {UseMutationResult}
+ */
+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}
+ * @returns {UseMutationResult}
+ */
+export function useUpdateBankAccount(
+ options?: UseMutationOptions<
+ UpdateBankAccountRes,
+ Error,
+ UpdateBankAccountValues
+ >,
+): UseMutationResult {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ ({ bankAccountId }) =>
+ apiRequest.post(`/banking/bank_accounts/${bankAccountId}/update`),
+ {
+ ...options,
+ onSuccess: () => {},
+ },
+ );
+}
+
interface EditBankRuleValues {
id: number;
value: any;
diff --git a/packages/webapp/src/hooks/query/subscription.tsx b/packages/webapp/src/hooks/query/subscription.tsx
new file mode 100644
index 000000000..d3d0ebc4e
--- /dev/null
+++ b/packages/webapp/src/hooks/query/subscription.tsx
@@ -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} options -
+ * @returns {UseMutationResult}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} options -
+ * @returns {UseMutationResult}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} options -
+ * @returns {UseMutationResult}
+ */
+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;
+}
+
+/**
+ * Changese the main subscription of the current organization.
+ * @param {UseMutationOptions} options -
+ * @returns {UseMutationResult}
+ */
+export function useGetSubscriptions(
+ options?: UseQueryOptions<
+ GetSubscriptionsQuery,
+ Error,
+ GetSubscriptionsResponse
+ >,
+): UseQueryResult {
+ const apiRequest = useApiRequest();
+
+ return useQuery(
+ [QueryKeys.Subscriptions],
+ (values) =>
+ apiRequest
+ .get(`/subscription`)
+ .then((res) => transformToCamelCase(res.data)),
+ {
+ ...options,
+ },
+ );
+}
diff --git a/packages/webapp/src/routes/dashboard.tsx b/packages/webapp/src/routes/dashboard.tsx
index b1b4cb1d4..aaedb5853 100644
--- a/packages/webapp/src/routes/dashboard.tsx
+++ b/packages/webapp/src/routes/dashboard.tsx
@@ -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: `/`,
diff --git a/packages/webapp/src/static/json/icons.tsx b/packages/webapp/src/static/json/icons.tsx
index 2e3d19ed7..4149a8c70 100644
--- a/packages/webapp/src/static/json/icons.tsx
+++ b/packages/webapp/src/static/json/icons.tsx
@@ -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',
+ },
};