mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 15:50:32 +00:00
feat: wip billing page
This commit is contained in:
@@ -6,6 +6,165 @@ export class GetSubscriptionsTransformer extends Transformer {
|
|||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return [];
|
return [
|
||||||
|
'canceledAtFormatted',
|
||||||
|
'cancelsAtFormatted',
|
||||||
|
'trialStartsAtFormatted',
|
||||||
|
'trialEndsAtFormatted',
|
||||||
|
'statusFormatted',
|
||||||
|
'planName',
|
||||||
|
'planSlug',
|
||||||
|
'planPrice',
|
||||||
|
'planPriceCurrency',
|
||||||
|
'planPriceFormatted',
|
||||||
|
'planPeriod',
|
||||||
|
'lemonUrls',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['id', 'plan'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the canceled at formatted.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public canceledAtFormatted = (subscription) => {
|
||||||
|
return subscription.canceledAt
|
||||||
|
? this.formatDate(subscription.canceledAt)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the cancels at formatted.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public cancelsAtFormatted = (subscription) => {
|
||||||
|
return subscription.cancelsAt
|
||||||
|
? this.formatDate(subscription.cancelsAt)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the trial starts at formatted date.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public trialStartsAtFormatted = (subscription) => {
|
||||||
|
return subscription.trialStartsAt
|
||||||
|
? this.formatDate(subscription.trialStartsAt)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the trial ends at formatted date.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public trialEndsAtFormatted = (subscription) => {
|
||||||
|
return subscription.trialEndsAt
|
||||||
|
? this.formatDate(subscription.trialEndsAt)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the Lemon subscription metadata.
|
||||||
|
* @param subscription
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public lemonSubscription = (subscription) => {
|
||||||
|
return (
|
||||||
|
this.options.lemonSubscriptions[subscription.lemonSubscriptionId] || null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted subscription status.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public statusFormatted = (subscription) => {
|
||||||
|
const pairs = {
|
||||||
|
canceled: 'Canceled',
|
||||||
|
active: 'Active',
|
||||||
|
inactive: 'Inactive',
|
||||||
|
expired: 'Expired',
|
||||||
|
on_trial: 'On Trial',
|
||||||
|
};
|
||||||
|
return pairs[subscription.status] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan name.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planName(subscription) {
|
||||||
|
return subscription.plan?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan slug.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planSlug(subscription) {
|
||||||
|
return subscription.plan?.slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan price.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
public planPrice(subscription) {
|
||||||
|
return subscription.plan?.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan price currency.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planPriceCurrency(subscription) {
|
||||||
|
return subscription.plan?.currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan formatted price.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planPriceFormatted(subscription) {
|
||||||
|
return this.formatMoney(subscription.plan?.price, {
|
||||||
|
currencyCode: subscription.plan?.currency,
|
||||||
|
precision: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan period.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planPeriod(subscription) {
|
||||||
|
return subscription?.plan?.period;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the subscription Lemon Urls.
|
||||||
|
* @param subscription
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public lemonUrls = (subscription) => {
|
||||||
|
const lemonSusbcription = this.lemonSubscription(subscription);
|
||||||
|
console.log(lemonSusbcription);
|
||||||
|
|
||||||
|
return lemonSusbcription?.data?.attributes?.urls;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { configureLemonSqueezy } from './utils';
|
import { configureLemonSqueezy } from './utils';
|
||||||
import { PlanSubscription } from '@/system/models';
|
import { PlanSubscription } from '@/system/models';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { getSubscription } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import { PlanSubscription } from '@/system/models';
|
import { PlanSubscription } from '@/system/models';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetSubscriptionsTransformer } from './GetSubscriptionsTransformer';
|
import { GetSubscriptionsTransformer } from './GetSubscriptionsTransformer';
|
||||||
|
import { configureLemonSqueezy } from './utils';
|
||||||
|
import { fromPairs } from 'lodash';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
@@ -13,14 +17,34 @@ export default class SubscriptionService {
|
|||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
*/
|
*/
|
||||||
public async getSubscriptions(tenantId: number) {
|
public async getSubscriptions(tenantId: number) {
|
||||||
const subscriptions = await PlanSubscription.query().where(
|
configureLemonSqueezy();
|
||||||
'tenant_id',
|
|
||||||
tenantId
|
const subscriptions = await PlanSubscription.query()
|
||||||
|
.where('tenant_id', tenantId)
|
||||||
|
.withGraphFetched('plan');
|
||||||
|
|
||||||
|
const lemonSubscriptionsResult = await PromisePool.withConcurrency(1)
|
||||||
|
.for(subscriptions)
|
||||||
|
.process(async (subscription, index, pool) => {
|
||||||
|
if (subscription.lemonSubscriptionId) {
|
||||||
|
const res = await getSubscription(subscription.lemonSubscriptionId);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return [subscription.lemonSubscriptionId, res.data];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lemonSubscriptions = fromPairs(
|
||||||
|
lemonSubscriptionsResult?.results.filter((result) => !!result[1])
|
||||||
);
|
);
|
||||||
return this.transformer.transform(
|
return this.transformer.transform(
|
||||||
tenantId,
|
tenantId,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
new GetSubscriptionsTransformer()
|
new GetSubscriptionsTransformer(),
|
||||||
|
{
|
||||||
|
lemonSubscriptions,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('subscription_plan_subscriptions', (table) => {
|
||||||
|
table.dateTime('trial_starts_at').nullable();
|
||||||
|
table.dateTime('trial_ends_at').nullable();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.table('subscription_plan_subscriptions', (table) => {
|
||||||
|
table.dropColumn('trial_starts_at').nullable();
|
||||||
|
table.dropColumn('trial_ends_at').nullable();
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -5,7 +5,16 @@ import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
|||||||
|
|
||||||
export default class PlanSubscription extends mixin(SystemModel) {
|
export default class PlanSubscription extends mixin(SystemModel) {
|
||||||
lemonSubscriptionId: number;
|
lemonSubscriptionId: number;
|
||||||
|
|
||||||
|
canceledAt: Date;
|
||||||
|
cancelsAt: Date;
|
||||||
|
|
||||||
|
trialStartsAt: Date;
|
||||||
|
trialEndsAt: Date;
|
||||||
|
|
||||||
|
endsAt: Date;
|
||||||
|
startsAt: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
@@ -24,7 +33,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
* Defined virtual attributes.
|
* Defined virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['active', 'inactive', 'ended', 'onTrial'];
|
return ['active', 'inactive', 'ended', 'canceled', 'onTrial', 'status'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +49,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
builder.where('trial_ends_at', '>', now);
|
builder.where('trial_ends_at', '>', now);
|
||||||
},
|
},
|
||||||
|
|
||||||
inactiveSubscriptions() {
|
inactiveSubscriptions(builder) {
|
||||||
builder.modify('endedTrial');
|
builder.modify('endedTrial');
|
||||||
builder.modify('endedPeriod');
|
builder.modify('endedPeriod');
|
||||||
},
|
},
|
||||||
@@ -100,35 +109,80 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if subscription is active.
|
* Check if the subscription is expired.
|
||||||
|
* Expired mens the user his lost the right to use the product.
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
public expired() {
|
||||||
|
return this.ended() && !this.onTrial();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if paid subscription is active.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
active() {
|
public active() {
|
||||||
return !this.ended() || this.onTrial();
|
return (
|
||||||
|
!this.canceled() && !this.onTrial() && !this.ended() && this.started()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if subscription is inactive.
|
* Check if subscription is inactive.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
inactive() {
|
public inactive() {
|
||||||
return !this.active();
|
return !this.active();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if subscription period has ended.
|
* Check if paid subscription period has ended.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
ended() {
|
public ended() {
|
||||||
return this.endsAt ? moment().isAfter(this.endsAt) : false;
|
return this.endsAt ? moment().isAfter(this.endsAt) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the paid subscription has started.
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
public started() {
|
||||||
|
return this.startsAt ? moment().isAfter(this.startsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if subscription is currently on trial.
|
* Check if subscription is currently on trial.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
onTrial() {
|
public onTrial() {
|
||||||
return this.trailEndsAt ? moment().isAfter(this.trailEndsAt) : false;
|
return this.trialEndsAt ? moment().isBefore(this.trialEndsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the subscription is canceled.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public canceled() {
|
||||||
|
return (
|
||||||
|
this.canceledAt ||
|
||||||
|
(this.cancelsAt && moment().isAfter(this.cancelsAt)) ||
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription status.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public status() {
|
||||||
|
return this.canceled()
|
||||||
|
? 'canceled'
|
||||||
|
: this.onTrial()
|
||||||
|
? 'on_trial'
|
||||||
|
: this.active()
|
||||||
|
? 'active'
|
||||||
|
: 'inactive';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,7 +197,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
const period = new SubscriptionPeriod(
|
const period = new SubscriptionPeriod(
|
||||||
invoiceInterval,
|
invoiceInterval,
|
||||||
invoicePeriod,
|
invoicePeriod,
|
||||||
start,
|
start
|
||||||
);
|
);
|
||||||
|
|
||||||
const startsAt = period.getStartDate();
|
const startsAt = period.getStartDate();
|
||||||
@@ -159,7 +213,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
renew(invoiceInterval, invoicePeriod) {
|
renew(invoiceInterval, invoicePeriod) {
|
||||||
const { startsAt, endsAt } = PlanSubscription.setNewPeriod(
|
const { startsAt, endsAt } = PlanSubscription.setNewPeriod(
|
||||||
invoiceInterval,
|
invoiceInterval,
|
||||||
invoicePeriod,
|
invoicePeriod
|
||||||
);
|
);
|
||||||
return this.$query().update({ startsAt, endsAt });
|
return this.$query().update({ startsAt, endsAt });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import RefundVendorCreditDetailDrawer from '@/containers/Drawers/RefundVendorCre
|
|||||||
import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer';
|
import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer';
|
||||||
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
||||||
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
|
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
|
||||||
|
import ChangeSubscriptionPlanDrawer from '@/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer';
|
||||||
|
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ export default function DrawersContainer() {
|
|||||||
/>
|
/>
|
||||||
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
||||||
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
|
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
|
||||||
|
<ChangeSubscriptionPlanDrawer name={DRAWERS.CHANGE_SUBSCARIPTION_PLAN} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ export enum DRAWERS {
|
|||||||
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
||||||
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
||||||
CATEGORIZE_TRANSACTION = 'categorize-transaction',
|
CATEGORIZE_TRANSACTION = 'categorize-transaction',
|
||||||
|
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface SubscriptionPricingProps {
|
|||||||
annuallyPriceLabel: string;
|
annuallyPriceLabel: string;
|
||||||
monthlyVariantId?: string;
|
monthlyVariantId?: string;
|
||||||
annuallyVariantId?: string;
|
annuallyVariantId?: string;
|
||||||
|
onSubscribe?: (variantId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubscriptionPricingCombinedProps
|
interface SubscriptionPricingCombinedProps
|
||||||
@@ -46,6 +47,7 @@ function SubscriptionPlanRoot({
|
|||||||
annuallyPriceLabel,
|
annuallyPriceLabel,
|
||||||
monthlyVariantId,
|
monthlyVariantId,
|
||||||
annuallyVariantId,
|
annuallyVariantId,
|
||||||
|
onSubscribe,
|
||||||
|
|
||||||
// #withPlans
|
// #withPlans
|
||||||
plansPeriod,
|
plansPeriod,
|
||||||
@@ -59,17 +61,19 @@ function SubscriptionPlanRoot({
|
|||||||
? monthlyVariantId
|
? monthlyVariantId
|
||||||
: annuallyVariantId;
|
: annuallyVariantId;
|
||||||
|
|
||||||
getLemonCheckout({ variantId })
|
onSubscribe && onSubscribe(variantId);
|
||||||
.then((res) => {
|
|
||||||
const checkoutUrl = res.data.data.attributes.url;
|
// getLemonCheckout({ variantId })
|
||||||
window.LemonSqueezy.Url.Open(checkoutUrl);
|
// .then((res) => {
|
||||||
})
|
// const checkoutUrl = res.data.data.attributes.url;
|
||||||
.catch(() => {
|
// window.LemonSqueezy.Url.Open(checkoutUrl);
|
||||||
AppToaster.show({
|
// })
|
||||||
message: 'Something went wrong!',
|
// .catch(() => {
|
||||||
intent: Intent.DANGER,
|
// AppToaster.show({
|
||||||
});
|
// message: 'Something went wrong!',
|
||||||
});
|
// intent: Intent.DANGER,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ interface BillingPageBootProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BillingPageBoot({ children }: BillingPageBootProps) {
|
export function BillingPageBoot({ children }: BillingPageBootProps) {
|
||||||
const { isLoading: isSubscriptionsLoading, data: subscriptions } =
|
const { isLoading: isSubscriptionsLoading, data: subscriptionsRes } =
|
||||||
useGetSubscriptions();
|
useGetSubscriptions();
|
||||||
|
|
||||||
|
const mainSubscription = subscriptionsRes?.subscriptions?.find(
|
||||||
|
(s) => s.slug === 'main',
|
||||||
|
);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
isSubscriptionsLoading,
|
isSubscriptionsLoading,
|
||||||
subscriptions,
|
subscriptions: subscriptionsRes?.subscriptions,
|
||||||
|
mainSubscription,
|
||||||
};
|
};
|
||||||
return <BillingBoot.Provider value={value}>{children}</BillingBoot.Provider>;
|
return <BillingBoot.Provider value={value}>{children}</BillingBoot.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
// @ts-nocheck
|
||||||
import { Box, Group } from '@/components';
|
import { Box, Group } from '@/components';
|
||||||
import { Text } from '@blueprintjs/core';
|
import { Spinner, Text } from '@blueprintjs/core';
|
||||||
import { Subscription } from './BillingSubscription';
|
import { Subscription } from './BillingSubscription';
|
||||||
|
import { useBillingPageBoot } from './BillingPageBoot';
|
||||||
import styles from './BillingPageContent.module.scss';
|
import styles from './BillingPageContent.module.scss';
|
||||||
|
|
||||||
export function BillingPageContent() {
|
export function BillingPageContent() {
|
||||||
|
const { isSubscriptionsLoading, subscriptions } = useBillingPageBoot();
|
||||||
|
|
||||||
|
if (isSubscriptionsLoading || !subscriptions) {
|
||||||
|
return <Spinner size={30} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.root}>
|
<Box className={styles.root}>
|
||||||
<Text>
|
<Text>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
.title{
|
.title{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #3D4C58;
|
color: #3D4C58;
|
||||||
}
|
}
|
||||||
@@ -56,8 +56,4 @@
|
|||||||
}
|
}
|
||||||
.actions{
|
.actions{
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|
||||||
button{
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,25 +4,42 @@ import { Box, Group, Stack } from '@/components';
|
|||||||
import { Button, Card, Intent, Text } from '@blueprintjs/core';
|
import { Button, Card, Intent, Text } from '@blueprintjs/core';
|
||||||
import withAlertActions from '../Alert/withAlertActions';
|
import withAlertActions from '../Alert/withAlertActions';
|
||||||
import styles from './BillingSubscription.module.scss';
|
import styles from './BillingSubscription.module.scss';
|
||||||
|
import withDrawerActions from '../Drawer/withDrawerActions';
|
||||||
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import { useBillingPageBoot } from './BillingPageBoot';
|
||||||
|
|
||||||
function SubscriptionRoot({ openAlert }) {
|
function SubscriptionRoot({ openAlert, openDrawer }) {
|
||||||
|
const { mainSubscription } = useBillingPageBoot();
|
||||||
|
|
||||||
|
// Can't continue if the main subscription is not loaded.
|
||||||
|
if (!mainSubscription) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const handleCancelSubBtnClick = () => {
|
const handleCancelSubBtnClick = () => {
|
||||||
openAlert('cancel-main-subscription');
|
openAlert('cancel-main-subscription');
|
||||||
};
|
};
|
||||||
const handleResumeSubBtnClick = () => {
|
const handleResumeSubBtnClick = () => {
|
||||||
openAlert('resume-main-subscription');
|
openAlert('resume-main-subscription');
|
||||||
};
|
};
|
||||||
const handleUpdatePaymentMethod = () => {};
|
const handleUpdatePaymentMethod = () => {
|
||||||
|
window.LemonSqueezy.Url.Open(
|
||||||
const handleUpgradeBtnClick = () => {};
|
mainSubscription.lemonUrls?.updatePaymentMethod,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// Handle upgrade button click.
|
||||||
|
const handleUpgradeBtnClick = () => {
|
||||||
|
openDrawer(DRAWERS.CHANGE_SUBSCARIPTION_PLAN);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={styles.root}>
|
<Card className={styles.root}>
|
||||||
<Stack spacing={8}>
|
<Stack spacing={6}>
|
||||||
<h1 className={styles.title}>Capital Essential</h1>
|
<h1 className={styles.title}>{mainSubscription.planName}</h1>
|
||||||
|
|
||||||
<Group spacing={0} className={styles.period}>
|
<Group spacing={0} className={styles.period}>
|
||||||
<Text className={styles.periodStatus}>Trial</Text>
|
<Text className={styles.periodStatus}>
|
||||||
|
{mainSubscription.statusFormatted}
|
||||||
|
</Text>
|
||||||
<Text className={styles.periodText}>Trial ends in 10 days.</Text>
|
<Text className={styles.periodText}>Trial ends in 10 days.</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -43,15 +60,29 @@ function SubscriptionRoot({ openAlert }) {
|
|||||||
>
|
>
|
||||||
Upgrade the Plan
|
Upgrade the Plan
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
minimal
|
{mainSubscription.canceled && (
|
||||||
small
|
<Button
|
||||||
inline
|
minimal
|
||||||
intent={Intent.PRIMARY}
|
small
|
||||||
onClick={handleCancelSubBtnClick}
|
inline
|
||||||
>
|
intent={Intent.PRIMARY}
|
||||||
Cancel Subscription
|
onClick={handleResumeSubBtnClick}
|
||||||
</Button>
|
>
|
||||||
|
Resume Subscription
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!mainSubscription.canceled && (
|
||||||
|
<Button
|
||||||
|
minimal
|
||||||
|
small
|
||||||
|
inline
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
onClick={handleCancelSubBtnClick}
|
||||||
|
>
|
||||||
|
Cancel Subscription
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
minimal
|
minimal
|
||||||
small
|
small
|
||||||
@@ -65,22 +96,38 @@ function SubscriptionRoot({ openAlert }) {
|
|||||||
|
|
||||||
<Group position={'apart'} style={{ marginTop: 'auto' }}>
|
<Group position={'apart'} style={{ marginTop: 'auto' }}>
|
||||||
<Group spacing={4}>
|
<Group spacing={4}>
|
||||||
<Text className={styles.priceAmount}>$10</Text>
|
<Text className={styles.priceAmount}>
|
||||||
<Text className={styles.pricePeriod}>/ mo</Text>
|
{mainSubscription.planPriceFormatted}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{mainSubscription.planPeriod && (
|
||||||
|
<Text className={styles.pricePeriod}>
|
||||||
|
{mainSubscription.planPeriod === 'month'
|
||||||
|
? 'mo'
|
||||||
|
: mainSubscription.planPeriod === 'year'
|
||||||
|
? 'yearly'
|
||||||
|
: ''}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
{mainSubscription.canceled && (
|
||||||
intent={Intent.PRIMARY}
|
<Button
|
||||||
onClick={handleResumeSubBtnClick}
|
intent={Intent.PRIMARY}
|
||||||
className={styles.subscribeButton}
|
onClick={handleResumeSubBtnClick}
|
||||||
>
|
className={styles.subscribeButton}
|
||||||
Resume Subscription
|
>
|
||||||
</Button>
|
Resume Subscription
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Subscription = R.compose(withAlertActions)(SubscriptionRoot);
|
export const Subscription = R.compose(
|
||||||
|
withAlertActions,
|
||||||
|
withDrawerActions,
|
||||||
|
)(SubscriptionRoot);
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Callout, Classes, Intent } from '@blueprintjs/core';
|
||||||
|
import { AppToaster, Box } from '@/components';
|
||||||
|
import { SubscriptionPlans } from '@/containers/Setup/SetupSubscription/SubscriptionPlans';
|
||||||
|
import { SubscriptionPlansPeriodSwitcher } from '@/containers/Setup/SetupSubscription/SubscriptionPlansPeriodSwitcher';
|
||||||
|
import { useChangeSubscriptionPlan } from '@/hooks/query/subscription';
|
||||||
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
|
||||||
|
function ChangeSubscriptionPlanContent({ closeDrawer }) {
|
||||||
|
const { mutateAsync: changeSubscriptionPlan } = useChangeSubscriptionPlan();
|
||||||
|
|
||||||
|
// Handle the subscribe button click.
|
||||||
|
const handleSubscribe = (variantId: number) => {
|
||||||
|
changeSubscriptionPlan({ variant_id: variantId })
|
||||||
|
.then(() => {
|
||||||
|
closeDrawer(DRAWERS.CHANGE_SUBSCARIPTION_PLAN);
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
message: 'The subscription plan has been changed successfully.',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 />
|
||||||
|
<SubscriptionPlans onSubscribe={handleSubscribe} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default R.compose(withDrawerActions)(ChangeSubscriptionPlanContent);
|
||||||
@@ -9,6 +9,11 @@ import {
|
|||||||
UseQueryResult,
|
UseQueryResult,
|
||||||
} from 'react-query';
|
} from 'react-query';
|
||||||
import useApiRequest from '../useRequest';
|
import useApiRequest from '../useRequest';
|
||||||
|
import { transformToCamelCase } from '@/utils';
|
||||||
|
|
||||||
|
const QueryKeys = {
|
||||||
|
Subscriptions: 'Subscriptions',
|
||||||
|
};
|
||||||
|
|
||||||
interface CancelMainSubscriptionValues {}
|
interface CancelMainSubscriptionValues {}
|
||||||
interface CancelMainSubscriptionResponse {}
|
interface CancelMainSubscriptionResponse {}
|
||||||
@@ -40,6 +45,9 @@ export function useCancelMainSubscription(
|
|||||||
(values) =>
|
(values) =>
|
||||||
apiRequest.post(`/subscription/cancel`, values).then((res) => res.data),
|
apiRequest.post(`/subscription/cancel`, values).then((res) => res.data),
|
||||||
{
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(QueryKeys.Subscriptions);
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -75,6 +83,9 @@ export function useResumeMainSubscription(
|
|||||||
(values) =>
|
(values) =>
|
||||||
apiRequest.post(`/subscription/resume`, values).then((res) => res.data),
|
apiRequest.post(`/subscription/resume`, values).then((res) => res.data),
|
||||||
{
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(QueryKeys.Subscriptions);
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -105,20 +116,58 @@ export function useChangeSubscriptionPlan(
|
|||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<
|
||||||
ChangeMainSubscriptionPlanValues,
|
ChangeMainSubscriptionPlanResponse,
|
||||||
Error,
|
Error,
|
||||||
ChangeMainSubscriptionPlanResponse
|
ChangeMainSubscriptionPlanValues
|
||||||
>(
|
>(
|
||||||
(values) =>
|
(values) =>
|
||||||
apiRequest.post(`/subscription/change`, values).then((res) => res.data),
|
apiRequest.post(`/subscription/change`, values).then((res) => res.data),
|
||||||
{
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(QueryKeys.Subscriptions);
|
||||||
|
},
|
||||||
...options,
|
...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 GetSubscriptionsQuery {}
|
||||||
interface GetSubscriptionsResponse {}
|
interface GetSubscriptionsResponse {
|
||||||
|
subscriptions: Array<LemonSubscription>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changese the main subscription of the current organization.
|
* Changese the main subscription of the current organization.
|
||||||
@@ -135,8 +184,11 @@ export function useGetSubscriptions(
|
|||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useQuery<GetSubscriptionsQuery, Error, GetSubscriptionsResponse>(
|
return useQuery<GetSubscriptionsQuery, Error, GetSubscriptionsResponse>(
|
||||||
['SUBSCRIPTIONS'],
|
[QueryKeys.Subscriptions],
|
||||||
(values) => apiRequest.get(`/subscription`).then((res) => res.data),
|
(values) =>
|
||||||
|
apiRequest
|
||||||
|
.get(`/subscription`)
|
||||||
|
.then((res) => transformToCamelCase(res.data)),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user