1
0
mirror of https://github.com/tabler/tabler.git synced 2026-07-15 01:51:43 +04:00

remove old lemon squeezy integration

This commit is contained in:
tabler.developer@gmail.com
2023-12-07 02:07:01 +01:00
parent 09a8e2d2c5
commit 95891cb108
10 changed files with 4 additions and 448 deletions
+1 -3
View File
@@ -48,6 +48,4 @@ _site
.contentlayer
public/static/tabler-icons/icons/*
public/static/tabler-icons/icons-png/*
certificates
public/static/tabler-icons/icons-png/*
@@ -1,12 +0,0 @@
import Products from '@/components/Products';
export const metadata = {
title: 'Subscription',
description: 'Tabler subscription',
};
export default function Subscription() {
return (
<Products/>
);
}
-63
View File
@@ -1,63 +0,0 @@
import { z } from 'zod'
const Attributes = z
.object({
pause: z.any().optional(),
status: z.enum(['on_trial', 'active', 'paused', 'past_due', 'unpaid', 'cancelled', 'expired']),
order_id: z.number(),
store_id: z.number(),
cancelled: z.boolean().nullable().optional(),
user_name: z.string(),
card_brand: z.enum(['visa', 'mastercard', 'american_express', 'discover', 'jcb', 'diners_club']),
product_id: z.number(),
variant_id: z.number(),
customer_id: z.number(),
product_name: z.string(),
variant_name: z.string(),
order_item_id: z.number(),
card_last_four: z.string(),
status_formatted: z.string(),
})
const Links = z
.object({
self: z.string(),
related: z.string().optional(),
})
const Order = z
.object({
links: Links,
})
const Relationships = z
.object({
order: Order,
store: Order,
product: Order,
variant: Order,
customer: Order,
'order-item': Order,
'subscription-invoices': Order,
})
const Data = z
.object({
id: z.coerce.number(),
type: z.string(),
links: Links,
attributes: Attributes,
relationships: Relationships,
})
const Meta = z
.object({
test_mode: z.boolean(),
event_name: z.string(),
})
export const SLemonSqueezyWebhookRequest = z
.object({
data: Data,
meta: Meta,
})
-79
View File
@@ -1,79 +0,0 @@
import prisma from '@/lib/prisma'
import crypto from 'crypto'
import { NextResponse } from 'next/server'
import { SLemonSqueezyWebhookRequest } from './models'
export async function POST(request: Request) {
const secret = process.env.LEMON_SQUEEZY_SIGNING_SECRET
if (!secret) {
throw new Error('LEMON_SQUEEZY_SIGNING_SECRET is not set')
}
const rawBody = await request.text()
if (!rawBody) {
throw new Error('No body')
}
const xSignature = request.headers.get('X-Signature')
const hmac = crypto.createHmac('sha256', secret)
hmac.update(rawBody)
const digest = hmac.digest('hex')
if (
!xSignature ||
!crypto.timingSafeEqual(Buffer.from(digest, 'hex'), Buffer.from(xSignature, 'hex'))
) {
throw new Error('Invalid signature.')
}
const body = JSON.parse(rawBody)
const type = body.data.type
if (type === 'subscriptions') {
const parsedBody = SLemonSqueezyWebhookRequest.parse(body)
if (parsedBody.meta.event_name === 'subscription_created') {
const createdSubscription = await prisma.subscription.create({
data: {
id: parsedBody.data.id,
...parsedBody.data.attributes,
},
select: {
id: true,
},
});
console.log(`Inserted subscription with id ${createdSubscription.id}`)
return NextResponse.json({
status: 'ok',
})
}
if (parsedBody.meta.event_name === 'subscription_updated') {
const updatedSubscription = await prisma.subscription.update({
where: {
id: parsedBody.data.id,
},
data: {
id: parsedBody.data.id,
...parsedBody.data.attributes,
},
select: {
id: true,
},
});
console.log(`Updated subscription with id: ${updatedSubscription.id}`)
return NextResponse.json({
status: 'ok',
})
}
}
}
-110
View File
@@ -1,110 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import clsx from 'clsx';
import { getProductVariants } from '@/lib/lemon-squeezy';
import Icon from '@/components/Icon';
import { useSession } from 'next-auth/react';
export default function ProductVariants({ productId }: { productId: string }) {
const { data: session } = useSession();
const [productVariants, setProductVariants] = useState<any>(null)
useEffect(() => {
fetchProductsVariants(productId)
}, []);
const fetchProductsVariants = async (productId: string) => {
const productVariants = await getProductVariants(productId)
setProductVariants(productVariants)
};
const formatLemonPrice = (price: number) => {
const priceString = price.toString();
const dollars = priceString.substring(0, priceString.length-2);
const cents = priceString.substring(priceString.length-2);
if (cents === '00') return dollars;
return `${dollars}.${cents}`;
}
const formatLemonVariantDescription = (description: string) => {
if (!description) return;
const pricingFeatures = description
.replaceAll('<p>','')
.replaceAll('</p>','')
.split('<br>')
return <ul className="pricing-features">
{
pricingFeatures.map((pricingFeature) => (
<li key={pricingFeature}>
<Icon name="check" className="text-green mr-2" />
{pricingFeature}
</li>
))
}
</ul>
}
const isFeatured = (variant) => {
return variant.attributes.interval === 'year' && variant.attributes.is_subscription
}
const createCheckoutLink = (variantSlug) => {
const url = new URL(`https://buy.tabler.io/checkout/buy/${variantSlug}?embed=1`)
if (!session?.user) return ''
const email = session?.user?.email
const name = session?.user?.name
// const userId = session?.userId
// url.searchParams.append('checkout[custom][user_id]', userId)
if (email) url.searchParams.append('checkout[email]', email)
if (name) url.searchParams.append('checkout[name]', name)
return url.toString()
}
return (
<div className="pricing">
{
productVariants &&
productVariants.data.map(variant => (
<div key={variant.id} className={clsx('pricing-card', { featured: isFeatured(variant) })}>
{
isFeatured(variant) &&
<div className="pricing-label">
<div className="label label-primary label-sm">Popular</div>
</div>
}
<h4 className="pricing-title">{variant.attributes.name}</h4>
<div className="pricing-price">
<span className="pricing-price-currency">$</span>
{ formatLemonPrice(variant.attributes.price) }
{
variant.attributes.is_subscription &&
<div className="pricing-price-description">
<div>per {variant.attributes.interval}</div>
</div>
}
</div>
{formatLemonVariantDescription(variant.attributes.description)}
<div className="pricing-btn">
<a
href={createCheckoutLink(variant.attributes.slug)}
className={clsx('btn btn-block', {
'btn-primary': isFeatured(variant) || productVariants.data.length === 1
})}
>
Get started
</a>
</div>
</div>
))}
</div>
);
}
-44
View File
@@ -1,44 +0,0 @@
'use client';
import { getProducts } from '@/lib/lemon-squeezy';
import { useState, useEffect } from 'react';
import ProductVariants from './ProductVariants';
export default function Products() {
const [products, setProducts] = useState<any>(null)
useEffect(() => {
fetchProducts()
}, []);
const fetchProducts = async () => {
const products = await getProducts()
setProducts(products)
};
const formatLemonProductDescription = (description: string) => {
if (!description) return;
return description
.replaceAll('<p>','')
.replaceAll('</p>','')
}
return (<>{
products &&
products.data.map(product => {
return <section key={product.id} className="section">
<div className="container">
<div className="section-header">
<h2 className="page-title">{product.attributes.name}</h2>
<p className="section-description">
{formatLemonProductDescription(product.attributes.description)}
</p>
</div>
<ProductVariants productId={product.id}/>
</div>
</section>
})
}</>);
}
+2 -2
View File
@@ -248,13 +248,13 @@ const NavigationAuth = () => {
</Popover.Button>
<Popover.Panel className="navbar-dropdown-menu">
<div className="navbar-dropdown-menu-content">
<div onClick={() => router.push('subscription')} className="navbar-dropdown-menu-link">
<div onClick={() => router.push('billing')} className="navbar-dropdown-menu-link">
<div className="row items-center g-3">
<div className="col-auto">
<Shape icon='rocket'/>
</div>
<div className="col">
<h5>Subscription</h5>
<h5>Billing</h5>
</div>
</div>
</div>
-55
View File
@@ -1,55 +0,0 @@
"use server"
import { SLemonSqueezyRequest, TLemonSqueezyRequest } from "./zod-lemon-squeezy";
const lemonSqueezyBaseUrl = 'https://api.lemonsqueezy.com/v1';
const lemonSqueezyApiKey = process.env.LEMON_SQUEEZY_API_KEY;
if (!lemonSqueezyApiKey) throw new Error("No LEMON_SQUEEZY_API_KEY environment variable set");
function createHeaders() {
const headers = new Headers();
headers.append('Accept', 'application/vnd.api+json');
headers.append('Content-Type', 'application/vnd.api+json');
headers.append('Authorization', `Bearer ${lemonSqueezyApiKey}`);
return headers;
}
function createRequestOptions(method: string, headers: Headers): RequestInit {
return {
method,
headers,
redirect: 'follow',
cache: "no-store"
};
}
export async function getProductVariants(productId: string): Promise<TLemonSqueezyRequest> {
const url = `${lemonSqueezyBaseUrl}/variants?filter[product_id]=${productId}`;
const headers = createHeaders();
const requestOptions = createRequestOptions('GET', headers);
const response: Response = await fetch(url, requestOptions);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
const parsedData = SLemonSqueezyRequest.parse(data);
return parsedData;
}
export async function getProducts(): Promise<TLemonSqueezyRequest> {
const url = `${lemonSqueezyBaseUrl}/products`;
const headers = createHeaders();
const requestOptions = createRequestOptions('GET', headers);
const response: Response = await fetch(url, requestOptions);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
const parsedData = SLemonSqueezyRequest.parse(data);
return parsedData;
}
-79
View File
@@ -1,79 +0,0 @@
import { z } from 'zod';
const SPagination = z.object({
currentPage: z.number(),
from: z.number(),
lastPage: z.number(),
perPage: z.number(),
to: z.number(),
total: z.number(),
});
const SMeta = z.object({
page: SPagination,
});
const SJsonapi = z.object({
version: z.string(),
});
const SLinks = z.object({
first: z.string(),
last: z.string(),
});
const SProductRelationships = z.object({
links: z.object({
related: z.string(),
self: z.string(),
}),
});
const SAttributes = z.object({
product_id: z.number().optional(),
name: z.string(),
slug: z.string(),
description: z.string().nullable(),
price: z.number(),
is_subscription: z.boolean().optional(),
interval: z.string().nullable().optional(),
interval_count: z.number().nullable().optional(),
has_free_trial: z.boolean().optional(),
trial_interval: z.string().optional(),
trial_interval_count: z.number().optional(),
pay_what_you_want: z.boolean(),
min_price: z.number().optional(),
suggested_price: z.number().optional(),
has_license_keys: z.boolean().optional(),
license_activation_limit: z.number().optional(),
is_license_limit_unlimited: z.boolean().optional(),
license_length_value: z.number().optional(),
license_length_unit: z.string().optional(),
is_license_length_unlimited: z.boolean().optional(),
sort: z.number().optional(),
status: z.string(),
status_formatted: z.string(),
created_at: z.string(),
updated_at: z.string(),
});
const SVariants = z.object({
type: z.string(),
id: z.string(),
attributes: SAttributes,
relationships: z.object({
product: SProductRelationships.optional(),
}),
links: z.object({
self: z.string(),
}),
});
export const SLemonSqueezyRequest = z.object({
meta: SMeta,
jsonapi: SJsonapi,
links: SLinks,
data: z.array(SVariants),
});
export type TLemonSqueezyRequest = z.infer<typeof SLemonSqueezyRequest>;
+1 -1
View File
@@ -1,3 +1,3 @@
export { default } from 'next-auth/middleware';
export const config = { matcher: ['/subscription'] };
export const config = { matcher: ['/billing'] };