diff --git a/site/.gitignore b/site/.gitignore index 865eabcf8..dad4de6ba 100644 --- a/site/.gitignore +++ b/site/.gitignore @@ -48,6 +48,4 @@ _site .contentlayer public/static/tabler-icons/icons/* -public/static/tabler-icons/icons-png/* - -certificates \ No newline at end of file +public/static/tabler-icons/icons-png/* \ No newline at end of file diff --git a/site/app/(marketing)/subscription/page.tsx b/site/app/(marketing)/subscription/page.tsx deleted file mode 100644 index c525c7089..000000000 --- a/site/app/(marketing)/subscription/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Products from '@/components/Products'; - -export const metadata = { - title: 'Subscription', - description: 'Tabler subscription', -}; - -export default function Subscription() { - return ( - - ); -} diff --git a/site/app/api/lemon-squeezy/models.ts b/site/app/api/lemon-squeezy/models.ts deleted file mode 100644 index 1d83cfae7..000000000 --- a/site/app/api/lemon-squeezy/models.ts +++ /dev/null @@ -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, - }) \ No newline at end of file diff --git a/site/app/api/lemon-squeezy/route.ts b/site/app/api/lemon-squeezy/route.ts deleted file mode 100644 index 14f14f6d6..000000000 --- a/site/app/api/lemon-squeezy/route.ts +++ /dev/null @@ -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', - }) - } - } -} \ No newline at end of file diff --git a/site/components/ProductVariants.tsx b/site/components/ProductVariants.tsx deleted file mode 100644 index 9ceb37993..000000000 --- a/site/components/ProductVariants.tsx +++ /dev/null @@ -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(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('

','') - .replaceAll('

','') - .split('
') - return - } - - 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 ( -
- { - productVariants && - productVariants.data.map(variant => ( -
- { - isFeatured(variant) && -
-
Popular
-
- } - -

{variant.attributes.name}

- -
- $ - { formatLemonPrice(variant.attributes.price) } - { - variant.attributes.is_subscription && -
-
per {variant.attributes.interval}
-
- } -
- - {formatLemonVariantDescription(variant.attributes.description)} - -
- - Get started - -
-
- ))} -
- ); -} diff --git a/site/components/Products.tsx b/site/components/Products.tsx deleted file mode 100644 index bd35c95c0..000000000 --- a/site/components/Products.tsx +++ /dev/null @@ -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(null) - - useEffect(() => { - fetchProducts() - }, []); - - const fetchProducts = async () => { - const products = await getProducts() - setProducts(products) - }; - - const formatLemonProductDescription = (description: string) => { - if (!description) return; - return description - .replaceAll('

','') - .replaceAll('

','') - } - - return (<>{ - products && - products.data.map(product => { - return
-
-
-

{product.attributes.name}

-

- {formatLemonProductDescription(product.attributes.description)} -

-
- - -
-
- }) - }); -} - diff --git a/site/components/layout/Header.tsx b/site/components/layout/Header.tsx index a43c55778..da455a362 100644 --- a/site/components/layout/Header.tsx +++ b/site/components/layout/Header.tsx @@ -248,13 +248,13 @@ const NavigationAuth = () => {
-
router.push('subscription')} className="navbar-dropdown-menu-link"> +
router.push('billing')} className="navbar-dropdown-menu-link">
-
Subscription
+
Billing
diff --git a/site/lib/lemon-squeezy.ts b/site/lib/lemon-squeezy.ts deleted file mode 100644 index ee88d1b8b..000000000 --- a/site/lib/lemon-squeezy.ts +++ /dev/null @@ -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 { - 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 { - 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; -} \ No newline at end of file diff --git a/site/lib/zod-lemon-squeezy.ts b/site/lib/zod-lemon-squeezy.ts deleted file mode 100644 index 56630dc77..000000000 --- a/site/lib/zod-lemon-squeezy.ts +++ /dev/null @@ -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; \ No newline at end of file diff --git a/site/middleware.ts b/site/middleware.ts index d118a7282..e233202ea 100644 --- a/site/middleware.ts +++ b/site/middleware.ts @@ -1,3 +1,3 @@ export { default } from 'next-auth/middleware'; -export const config = { matcher: ['/subscription'] }; \ No newline at end of file +export const config = { matcher: ['/billing'] }; \ No newline at end of file