Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upgrade the subscription plans #526

Merged
merged 3 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 3 additions & 22 deletions packages/server/src/services/Subscription/LemonSqueezyWebhooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
import config from '@/config';
import { Inject, Service } from 'typedi';
import {
Expand All @@ -10,15 +9,14 @@ import {
} from './utils';
import { Plan } from '@/system/models';
import { Subscription } from './Subscription';
import { isEmpty } from 'lodash';

@Service()
export class LemonSqueezyWebhooks {
@Inject()
private subscriptionService: Subscription;

/**
* handle the LemonSqueezy webhooks.
* Handles the Lemon Squeezy webhooks.
* @param {string} rawBody
* @param {string} signature
* @returns {Promise<void>}
Expand Down Expand Up @@ -74,34 +72,17 @@ export class LemonSqueezyWebhooks {
const variantId = attributes.variant_id as string;

// We assume that the Plan table is up to date.
const plan = await Plan.query().findOne('slug', 'early-adaptor');
const plan = await Plan.query().findOne('lemonVariantId', variantId);

if (!plan) {
throw new Error(`Plan with variantId ${variantId} not found.`);
} else {
// Update the subscription in the database.
const priceId = attributes.first_subscription_item.price_id;

// Get the price data from Lemon Squeezy.
const priceData = await getPrice(priceId);

if (priceData.error) {
throw new Error(
`Failed to get the price data for the subscription ${eventBody.data.id}.`
);
}
const isUsageBased =
attributes.first_subscription_item.is_usage_based;
const price = isUsageBased
? priceData.data?.data.attributes.unit_price_decimal
: priceData.data?.data.attributes.unit_price;

// Create a new subscription of the tenant.
if (webhookEvent === 'subscription_created') {
await this.subscriptionService.newSubscribtion(
tenantId,
'early-adaptor'
);
await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
}
}
} else if (webhookEvent.startsWith('order_')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('subscription_plans', (table) => {
table.string('lemon_variant_id').nullable().index();
});
};

exports.down = (knex) => {
return knex.schema.table('subscription_plans', (table) => {
table.dropColumn('lemon_variant_id');
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
exports.up = function (knex) {
return knex('subscription_plans').insert([
// Capital Basic
{
name: 'Capital Basic (Monthly)',
slug: 'capital-basic-monthly',
price: 10,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446152',
// lemon_variant_id: '450016',
},
{
name: 'Capital Basic (Annually)',
slug: 'capital-basic-annually',
price: 90,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446153',
// lemon_variant_id: '450018',
},

// # Capital Essential
{
name: 'Capital Essential (Monthly)',
slug: 'capital-essential-monthly',
price: 20,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446155',
// lemon_variant_id: '450028',
},
{
name: 'Capital Essential (Annually)',
slug: 'capital-essential-annually',
price: 180,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446156',
// lemon_variant_id: '450029',
},

// # Capital Plus
{
name: 'Capital Plus (Monthly)',
slug: 'capital-plus-monthly',
price: 25,
active: true,
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446165',
// lemon_variant_id: '450031',
},
{
name: 'Capital Plus (Annually)',
slug: 'capital-plus-annually',
price: 228,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446164',
// lemon_variant_id: '450032',
},

// # Capital Big
{
name: 'Capital Big (Monthly)',
slug: 'capital-big-monthly',
price: 40,
active: true,
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446167',
// lemon_variant_id: '450024',
},
{
name: 'Capital Big (Annually)',
slug: 'capital-big-annually',
price: 360,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446168',
// lemon_variant_id: '450025',
},
]);
};

exports.down = function (knex) {};
27 changes: 23 additions & 4 deletions packages/webapp/src/components/PricingPlan/PricingPlan.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
color: #fff;
text-align: center;
font-size: 12px;
text-transform: uppercase;
}
.label {
font-size: 14px;
font-size: 16px;
font-weight: 600;
color: #2F343C;

Expand All @@ -47,13 +48,31 @@
}
.price {
font-size: 18px;
line-height: 1;
font-weight: 500;
color: #404854;
line-height: 1;
font-weight: 500;
color: #252A31;
}

.pricePer{
color: #738091;
font-size: 12px;
line-height: 1;
}

.featureItem{
flex: 1;
color: #1C2127;
}

.featurePopover :global .bp4-popover-content{
border-radius: 0;
}
.featurePopoverContent{
font-size: 12px
}
.featurePopoverLabel {
text-transform: uppercase;
letter-spacing: 0.4px;
font-size: 12px;
font-weight: 500;
}
45 changes: 39 additions & 6 deletions packages/webapp/src/components/PricingPlan/PricingPlan.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Button, ButtonProps, Intent } from '@blueprintjs/core';
import {
Button,
ButtonProps,
Intent,
Position,
Text,
Tooltip,
} from '@blueprintjs/core';
import clsx from 'classnames';
import { Box, Group, Stack } from '../Layout';
import styles from './PricingPlan.module.scss';
Expand Down Expand Up @@ -64,7 +71,7 @@ export interface PricingPriceProps {
*/
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
return (
<Stack spacing={6} className={styles.priceRoot}>
<Stack spacing={4} className={styles.priceRoot}>
<h4 className={styles.price}>{price}</h4>
<span className={styles.pricePer}>{subPrice}</span>
</Stack>
Expand Down Expand Up @@ -101,23 +108,49 @@ export interface PricingFeaturesProps {
*/
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
return (
<Stack spacing={10} className={styles.features}>
<Stack spacing={14} className={styles.features}>
{children}
</Stack>
);
};

export interface PricingFeatureLineProps {
children: React.ReactNode;
hintContent?: string;
hintLabel?: string;
}

/**
* Displays a single feature line within a list of features.
* @param children - The content of the feature line.
*/
PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => {
return (
<Group noWrap spacing={12}>
PricingPlan.FeatureLine = ({
children,
hintContent,
hintLabel,
}: PricingFeatureLineProps) => {
return hintContent ? (
<Tooltip
content={
<Stack spacing={5}>
{hintLabel && (
<Text className={styles.featurePopoverLabel}>{hintLabel}</Text>
)}
<Text className={styles.featurePopoverContent}>{hintContent}</Text>
</Stack>
}
position={Position.TOP_LEFT}
popoverClassName={styles.featurePopover}
modifiers={{ offset: { enabled: true, offset: '0,10' } }}
minimal
>
<Group noWrap spacing={8} style={{ cursor: 'help' }}>
<CheckCircled height={12} width={12} />
<Box className={styles.featureItem}>{children}</Box>
</Group>
</Tooltip>
) : (
<Group noWrap spacing={8}>
<CheckCircled height={12} width={12} />
<Box className={styles.featureItem}>{children}</Box>
</Group>
Expand Down
Loading
Loading