diff --git a/app/credits/page.tsx b/app/credits/page.tsx new file mode 100644 index 0000000..618943e --- /dev/null +++ b/app/credits/page.tsx @@ -0,0 +1,34 @@ +import { RouteModal } from '@/components/route-modal'; +import { fetchAccountAICredits } from '@/lib/services/account'; +import { getPrices } from '@/lib/services/stripe/client'; +import { + CalloutIcon, + CalloutRoot, + CalloutText, + Flex, + Heading, +} from '@radix-ui/themes'; +import { CgInfo } from 'react-icons/cg'; + +export default async function Page() { + const _prices = await getPrices(); + const credits = await fetchAccountAICredits(); + + return ( + + + + Your credits + + + + + + + + You have {credits} credits remaining. + + + + ); +} diff --git a/lib/errors.ts b/lib/errors.ts index 2d73809..e9403df 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -80,3 +80,14 @@ export class DatabaseError extends ExtendableError { this.underlyingError = error; } } + +export const createExternalServiceError = (serviceName: string) => + class ExternalServiceError extends ExtendableError { + serviceName = serviceName; + underlyingError: unknown; + + constructor(error: unknown) { + super(`Error from ${serviceName}`); + this.underlyingError = error; + } + }; diff --git a/lib/services/stripe/client.ts b/lib/services/stripe/client.ts new file mode 100644 index 0000000..57c4f9b --- /dev/null +++ b/lib/services/stripe/client.ts @@ -0,0 +1,23 @@ +import { env } from '@/env.mjs'; +import { createExternalServiceError } from '@/lib/errors'; +import { Stripe } from 'stripe'; + +const stripe = new Stripe(env.STRIPE_SECRET_KEY); +const StripeError = createExternalServiceError('Stripe'); + +const getProduct = async () => { + const products = await stripe.products.list(); + const product = products.data.find((p) => p.metadata.project === 'beecast'); + + if (!product) { + throw new StripeError('Product not found!'); + } + + return product; +}; + +export const getPrices = async () => { + const product = await getProduct(); + const prices = await stripe.prices.list({ product: product.id }); + return prices.data; +}; diff --git a/package.json b/package.json index 050561e..a75b5d2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react": "^18", "react-dom": "^18", "react-icons": "^4.12.0", + "stripe": "^14.9.0", "zod": "^3.22.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9aff200..1181b3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ dependencies: react-icons: specifier: ^4.12.0 version: 4.12.0(react@18.2.0) + stripe: + specifier: ^14.9.0 + version: 14.9.0 zod: specifier: ^3.22.4 version: 3.22.4 @@ -6328,7 +6331,6 @@ packages: function-bind: 1.1.2 get-intrinsic: 1.2.2 set-function-length: 1.1.1 - dev: true /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -7075,7 +7077,6 @@ packages: get-intrinsic: 1.2.2 gopd: 1.0.1 has-property-descriptors: 1.0.1 - dev: true /define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} @@ -8548,7 +8549,6 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true /function.prototype.name@1.1.6: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} @@ -8589,7 +8589,6 @@ packages: has-proto: 1.0.1 has-symbols: 1.0.3 hasown: 2.0.0 - dev: true /get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} @@ -8781,7 +8780,6 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.2 - dev: true /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -8837,17 +8835,14 @@ packages: resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} dependencies: get-intrinsic: 1.2.2 - dev: true /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} - dev: true /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} - dev: true /has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} @@ -8877,7 +8872,6 @@ packages: engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 - dev: true /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} @@ -10510,7 +10504,6 @@ packages: /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} - dev: true /object-is@1.1.5: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} @@ -11250,7 +11243,6 @@ packages: engines: {node: '>=0.6'} dependencies: side-channel: 1.0.4 - dev: true /querystring-es3@0.2.1: resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} @@ -11939,7 +11931,6 @@ packages: get-intrinsic: 1.2.2 gopd: 1.0.1 has-property-descriptors: 1.0.1 - dev: true /set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} @@ -12006,7 +11997,6 @@ packages: call-bind: 1.0.5 get-intrinsic: 1.2.2 object-inspect: 1.13.1 - dev: true /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -12377,6 +12367,14 @@ packages: engines: {node: '>=8'} dev: true + /stripe@14.9.0: + resolution: {integrity: sha512-t2XdpNbRH4x3MYEoxNWhwUPl9D80aUd5OHds0zhDiwRYPZ0H7MrUI/dj9wOSYlzycD3xdvjn0q7pWeFWljtMUQ==} + engines: {node: '>=12.*'} + dependencies: + '@types/node': 20.10.4 + qs: 6.11.2 + dev: false + /style-loader@3.3.3(webpack@5.89.0): resolution: {integrity: sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==} engines: {node: '>= 12.13.0'}