diff --git a/README.md b/README.md index c472757ad..9db7b4458 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,44 @@ # Makerkit - Supabase SaaS Starter Kit - Turbo Edition +This is a Starter Kit for building SaaS applications using Supabase, Next.js, and Tailwind CSS. + +This version uses Turborepo to manage multiple packages in a single repository. + +## Features + +- **Authentication**: Sign up, sign in, sign out, forgot password, update profile, and more. +- **Billing**: Subscription management, payment methods, invoices, and more. +- **Personal Account**: Manage your account, profile picture, and more. +- **Team Accounts**: Invite members, manage roles, and more. +- **Admin Dashboard**: Manage users, subscriptions, and more. +- **Pluggable**: Easily add new features and packages to your SaaS application. + +The most notable difference between this version and the original version is the use of Turborepo to manage multiple packages in a single repository. + +Thanks to Turborepo, we can manage and isolate different parts of the application in separate packages. This makes it easier to manage and scale the application as it grows. + +Additionally, we can extend the codebase without it impacting your web application. + +Let's get started! + +## Quick Start + +### 0. Prerequisites + +- Node.js 18.x or later +- Docker +- Pnpm +- Supabase account (optional for local development) +- Payment Gateway account (Stripe/Lemon Squeezy) +- Email Service account (optional for local development) + +#### 0.1. Install Pnpm + +```bash +# Install pnpm +npm i -g pnpm +``` + ### 1. Setup dependencies ```bash @@ -12,4 +51,68 @@ pnpm i ```bash # Start the development server pnpm dev -``` \ No newline at end of file +``` + +## Architecture + +This project uses Turborepo to manage multiple packages in a single repository. + +### Apps + +The core web application can be found in the `apps/web` package. + +Here is where we add the skeleton of the application, including the routing, layout, and global styles. + +The main application defines the following: +1. **Configuration**: Environment variables, feature flags, paths, and more. The configuration gets passed down to other packages. +2. **Routing**: The main routing of the application. Since this is file-based routing, we define the routes here. +3. **Local components**: Shared components that are used across the application but not necessarily shared with other apps/packages. +4. **Global styles**: Global styles that are used across the application. + +### Packages + +Below are the reusable packages that can be shared across multiple applications (or packages). + +- **`@kit/ui`**: Shared UI components and styles (using Shadcn UI) +- **`@kit/shared`**: Shared code and utilities +- **`@kit/supabase`**: Supabase package that defines the schema and logic for managing Supabase +- **`@kit/i18n`**: Internationalization package that defines utilities for managing translations +- **`@kit/billing`**: Billing package that defines the schema and logic for managing subscriptions +- **`@kit/billing-gateway`**: Billing gateway package that defines the schema and logic for managing payment gateways +- **`@kit/stripe`**: Stripe package that defines the schema and logic for managing Stripe. This is used by the `@kit/billing-gateway` package and abstracts the Stripe API. +- **`@kit/lemon-squeezy`**: Lemon Squeezy package that defines the schema and logic for managing Lemon Squeezy. This is used by the `@kit/billing-gateway` package and abstracts the Lemon Squeezy API. +- **`@kit/emails`**: Here we define the email templates using the `react.email` package. +- **`@kit/mailers`**: Mailer package that abstracts the email service provider (e.g., Resend, Cloudflare, SendGrid, Mailgun, etc.) + +And features that can be added to the application: +- **`@kit/auth`**: Authentication package (using Supabase) +- **`@kit/accounts`**: Package that defines components and logic for managing personal accounts +- **`@kit/team-accounts`**: Package that defines components and logic for managing team +- **`@kit/admin`**: Admin package that defines the schema and logic for managing users, subscriptions, and more. + +### Application Configuration + +The configuration is defined in the `apps/web/config` folder. Here you can find the following configuration files: +- **`app.config.ts`**: Application configuration (e.g., name, description, etc.) +- **`auth.config.ts`**: Authentication configuration +- **`billing.config.ts`**: Billing configuration +- **`feature-flags.config.ts`**: Feature flags configuration +- **`paths.config.ts`**: Paths configuration (e.g., routes, API paths, etc.) +- **`personal-account-sidebar.config.ts`**: Personal account sidebar configuration (e.g., links, icons, etc.) +- **`organization-account-sidebar.config.ts`**: Team account sidebar configuration (e.g., links, icons, etc.) + +## Installing a Shadcn UI component + +To install a Shadcn UI component, you can use the following command: + +```bash +npx shadcn-ui@latest add --path=packages/ui/shadcn +``` + +For example, to install the `Button` component, you can use the following command: + +```bash +npx shadcn-ui@latest add button --path=packages/rsc/ui/shadcn +``` + +We pass the `--path` flag to specify the path where the component should be installed. \ No newline at end of file diff --git a/apps/web/.env.development b/apps/web/.env.development index 087d17390..44a227b8b 100644 --- a/apps/web/.env.development +++ b/apps/web/.env.development @@ -33,4 +33,4 @@ NEXT_PUBLIC_LOCALES_PATH=apps/web/public/locales SIGN_IN_PATH=/auth/sign-in SIGN_UP_PATH=/auth/sign-up ORGANIZATION_ACCOUNTS_PATH=/home -INVITATION_PAGE_PATH=/invite +INVITATION_PAGE_PATH=/join \ No newline at end of file diff --git a/apps/web/app/(dashboard)/home/(user)/billing/components/personal-account-checkout-form.tsx b/apps/web/app/(dashboard)/home/(user)/billing/components/personal-account-checkout-form.tsx new file mode 100644 index 000000000..9508394bd --- /dev/null +++ b/apps/web/app/(dashboard)/home/(user)/billing/components/personal-account-checkout-form.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { useState, useTransition } from 'react'; + +import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@kit/ui/card'; + +import billingConfig from '~/config/billing.config'; + +import { createPersonalAccountCheckoutSession } from '../server-actions'; + +export function PersonalAccountCheckoutForm() { + const [pending, startTransition] = useTransition(); + const [checkoutToken, setCheckoutToken] = useState(null); + + // If the checkout token is set, render the embedded checkout component + if (checkoutToken) { + return ( + + ); + } + + // Otherwise, render the plan picker component + return ( +
+ + + Manage your Plan + + + You can change your plan at any time. + + + + + { + startTransition(async () => { + const { checkoutToken } = + await createPersonalAccountCheckoutSession({ + planId, + }); + + setCheckoutToken(checkoutToken); + }); + }} + /> + + +
+ ); +} diff --git a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx index 9a4864673..c51007310 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx @@ -1,8 +1,10 @@ -import { withI18n } from '~/lib/i18n/with-i18n'; - import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { withI18n } from '~/lib/i18n/with-i18n'; + +import { PersonalAccountCheckoutForm } from './components/personal-account-checkout-form'; + function PersonalAccountBillingPage() { return ( <> @@ -11,7 +13,9 @@ function PersonalAccountBillingPage() { description={} /> - + + + ); } diff --git a/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts b/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts new file mode 100644 index 000000000..9fd2afcc1 --- /dev/null +++ b/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts @@ -0,0 +1,123 @@ +'use server'; + +import { URL } from 'next/dist/compiled/@edge-runtime/primitives'; +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; + +import { z } from 'zod'; + +import { getProductPlanPairFromId } from '@kit/billing'; +import { getGatewayProvider } from '@kit/billing-gateway'; +import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; + +import billingConfig from '~/config/billing.config'; +import pathsConfig from '~/config/paths.config'; + +/** + * Creates a checkout session for a personal account. + * + * @param {object} params - The parameters for creating the checkout session. + * @param {string} params.planId - The ID of the plan to be associated with the account. + */ +export async function createPersonalAccountCheckoutSession(params: { + planId: string; +}) { + const client = getSupabaseServerActionClient(); + const { data, error } = await client.auth.getUser(); + + if (error ?? !data.user) { + throw new Error('Authentication required'); + } + + const planId = z.string().min(1).parse(params.planId); + const service = await getGatewayProvider(client); + const productPlanPairFromId = getProductPlanPairFromId(billingConfig, planId); + + if (!productPlanPairFromId) { + throw new Error('Product not found'); + } + + // in the case of personal accounts + // the account ID is the same as the user ID + const accountId = data.user.id; + + // the return URL for the checkout session + const returnUrl = getCheckoutSessionReturnUrl(); + + // find the customer ID for the account if it exists + // (eg. if the account has been billed before) + const customerId = await getCustomerIdFromAccountId(accountId); + + // retrieve the product and plan from the billing configuration + const { product, plan } = productPlanPairFromId; + + // call the payment gateway to create the checkout session + const { checkoutToken } = await service.createCheckoutSession({ + paymentType: product.paymentType, + returnUrl, + accountId, + planId, + trialPeriodDays: plan.trialPeriodDays, + customerEmail: data.user.email, + customerId, + }); + + // return the checkout token to the client + // so we can call the payment gateway to complete the checkout + return { + checkoutToken, + }; +} + +export async function createBillingPortalSession() { + const client = getSupabaseServerActionClient(); + const { data, error } = await client.auth.getUser(); + + if (error ?? !data.user) { + throw new Error('Authentication required'); + } + + const service = await getGatewayProvider(client); + + const accountId = data.user.id; + const customerId = await getCustomerIdFromAccountId(accountId); + const returnUrl = getBillingPortalReturnUrl(); + + const { url } = await service.createBillingPortalSession({ + customerId, + returnUrl, + }); + + return redirect(url); +} + +function getCheckoutSessionReturnUrl() { + const origin = headers().get('origin')!; + + return new URL( + pathsConfig.app.personalAccountBillingReturn, + origin, + ).toString(); +} + +function getBillingPortalReturnUrl() { + const origin = headers().get('origin')!; + + return new URL(pathsConfig.app.accountBilling, origin).toString(); +} + +async function getCustomerIdFromAccountId(accountId: string) { + const client = getSupabaseServerActionClient(); + + const { data, error } = await client + .from('billing_customers') + .select('customer_id') + .eq('account_id', accountId) + .maybeSingle(); + + if (error) { + throw error; + } + + return data?.customer_id; +} diff --git a/apps/web/app/join/[code]/page.tsx b/apps/web/app/join/page.tsx similarity index 82% rename from apps/web/app/join/[code]/page.tsx rename to apps/web/app/join/page.tsx index 1ba879012..f3c9c364b 100644 --- a/apps/web/app/join/[code]/page.tsx +++ b/apps/web/app/join/page.tsx @@ -3,19 +3,20 @@ import { notFound } from 'next/navigation'; import type { SupabaseClient } from '@supabase/supabase-js'; -import ExistingUserInviteForm from '~/join/_components/ExistingUserInviteForm'; -import NewUserInviteForm from '~/join/_components/NewUserInviteForm'; -import { withI18n } from '~/lib/i18n/with-i18n'; - import { Logger } from '@kit/shared/logger'; +import { Database } from '@kit/supabase/database'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { Heading } from '@kit/ui/heading'; import { If } from '@kit/ui/if'; import { Trans } from '@kit/ui/trans'; +import ExistingUserInviteForm from '~/join/_components/ExistingUserInviteForm'; +import NewUserInviteForm from '~/join/_components/NewUserInviteForm'; +import { withI18n } from '~/lib/i18n/with-i18n'; + interface Context { - params: { - code: string; + searchParams: { + invite_token: string; }; } @@ -23,9 +24,9 @@ export const metadata = { title: `Join Organization`, }; -async function InvitePage({ params }: Context) { - const code = params.code; - const data = await loadInviteData(code); +async function JoinTeamAccountPage({ searchParams }: Context) { + const token = searchParams.invite_token; + const data = await getInviteDataFromInviteToken(token); if (!data.membership) { notFound(); @@ -62,16 +63,19 @@ async function InvitePage({ params }: Context) {

- }> - {(session) => } + } + > + {(session) => } ); } -export default withI18n(InvitePage); +export default withI18n(JoinTeamAccountPage); -async function loadInviteData(code: string) { +async function getInviteDataFromInviteToken(code: string) { const client = getSupabaseServerComponentClient(); // we use an admin client to be able to read the pending membership diff --git a/apps/web/config/paths.config.ts b/apps/web/config/paths.config.ts index b578a5153..d35e6f6f4 100644 --- a/apps/web/config/paths.config.ts +++ b/apps/web/config/paths.config.ts @@ -2,21 +2,23 @@ import { z } from 'zod'; const PathsSchema = z.object({ auth: z.object({ - signIn: z.string(), - signUp: z.string(), - verifyMfa: z.string(), - callback: z.string(), - passwordReset: z.string(), - passwordUpdate: z.string(), + signIn: z.string().min(1), + signUp: z.string().min(1), + verifyMfa: z.string().min(1), + callback: z.string().min(1), + passwordReset: z.string().min(1), + passwordUpdate: z.string().min(1), }), app: z.object({ - home: z.string(), - personalAccountSettings: z.string(), - personalAccountBilling: z.string(), - accountHome: z.string(), - accountSettings: z.string(), - accountBilling: z.string(), - accountMembers: z.string(), + home: z.string().min(1), + personalAccountSettings: z.string().min(1), + personalAccountBilling: z.string().min(1), + personalAccountBillingReturn: z.string().min(1), + accountHome: z.string().min(1), + accountSettings: z.string().min(1), + accountBilling: z.string().min(1), + accountMembers: z.string().min(1), + accountBillingReturn: z.string().min(1), }), }); @@ -33,10 +35,12 @@ const pathsConfig = PathsSchema.parse({ home: '/home', personalAccountSettings: '/home/account', personalAccountBilling: '/home/billing', + personalAccountBillingReturn: '/home/billing/return', accountHome: '/home/[account]', accountSettings: `/home/[account]/settings`, accountBilling: `/home/[account]/billing`, accountMembers: `/home/[account]/members`, + accountBillingReturn: `/home/[account]/billing/return`, }, }); diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 2b702c989..96574a5a4 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -12,7 +12,7 @@ const config = { '@kit/i18n', '@kit/mailers', '@kit/billing', - '@kit/stripe', + '@kit/billing-gateway' ], pageExtensions: ['ts', 'tsx', 'mdx'], experimental: { diff --git a/apps/web/package.json b/apps/web/package.json index ad0deef8a..6504560ab 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,7 @@ "@kit/team-accounts": "^0.1.0", "@kit/supabase": "^0.1.0", "@kit/billing": "^0.1.0", + "@kit/billing-gateway": "^0.1.0", "@kit/mailers": "^0.1.0", "@hookform/resolvers": "^3.3.4", "@next/mdx": "^14.1.0", diff --git a/components.json b/components.json new file mode 100644 index 000000000..b282fe339 --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@kit/ui", + "utils": "@kit/ui/utils" + } +} \ No newline at end of file diff --git a/packages/billing-gateway/README.md b/packages/billing-gateway/README.md new file mode 100644 index 000000000..0cd1e8af1 --- /dev/null +++ b/packages/billing-gateway/README.md @@ -0,0 +1,3 @@ +# Billing - @kit/billing-gateway + +This package is responsible for handling all billing related operations. It is a gateway to the billing service. \ No newline at end of file diff --git a/packages/billing-gateway/package.json b/packages/billing-gateway/package.json new file mode 100644 index 000000000..67a0ff70b --- /dev/null +++ b/packages/billing-gateway/package.json @@ -0,0 +1,48 @@ +{ + "name": "@kit/billing-gateway", + "private": true, + "version": "0.1.0", + "scripts": { + "clean": "git clean -xdf .turbo node_modules", + "format": "prettier --check \"**/*.{ts,tsx}\"", + "lint": "eslint .", + "typecheck": "tsc --noEmit" + }, + "prettier": "@kit/prettier-config", + "exports": { + ".": "./src/index.ts", + "./components": "./src/components/index.ts" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "zod": "^3.22.4" + }, + "dependencies": { + "@kit/ui": "0.1.0", + "@kit/stripe": "0.1.0", + "@kit/billing": "0.1.0", + "@kit/supabase": "^0.1.0", + "lucide-react": "^0.361.0" + }, + "devDependencies": { + "@kit/prettier-config": "0.1.0", + "@kit/eslint-config": "0.2.0", + "@kit/tailwind-config": "0.1.0", + "@kit/tsconfig": "0.1.0" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@kit/eslint-config/base", + "@kit/eslint-config/react" + ] + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + } +} diff --git a/packages/billing-gateway/src/billing-gateway-factory.service.ts b/packages/billing-gateway/src/billing-gateway-factory.service.ts new file mode 100644 index 000000000..3979f5571 --- /dev/null +++ b/packages/billing-gateway/src/billing-gateway-factory.service.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; + +import { BillingProvider, BillingStrategyProviderService } from '@kit/billing'; + +export class BillingGatewayFactoryService { + static async GetProviderStrategy( + provider: z.infer, + ): Promise { + switch (provider) { + case 'stripe': { + const { StripeBillingStrategyService } = await import('@kit/stripe'); + + return new StripeBillingStrategyService(); + } + + case 'paddle': { + throw new Error('Paddle is not supported yet'); + } + + case 'lemon-squeezy': { + throw new Error('Lemon Squeezy is not supported yet'); + } + + default: + throw new Error(`Unsupported billing provider: ${provider as string}`); + } + } +} diff --git a/packages/billing-gateway/src/billing-gateway-service.ts b/packages/billing-gateway/src/billing-gateway-service.ts new file mode 100644 index 000000000..b0331e06a --- /dev/null +++ b/packages/billing-gateway/src/billing-gateway-service.ts @@ -0,0 +1,93 @@ +import { z } from 'zod'; + +import { BillingProvider } from '@kit/billing'; +import { + CancelSubscriptionParamsSchema, + CreateBillingCheckoutSchema, + CreateBillingPortalSessionSchema, + RetrieveCheckoutSessionSchema, +} from '@kit/billing/schema'; + +import { BillingGatewayFactoryService } from './billing-gateway-factory.service'; + +/** + * @description The billing gateway service to interact with the billing provider of choice (e.g. Stripe) + * @class BillingGatewayService + * @param {BillingProvider} provider - The billing provider to use + * @example + * + * const provider = 'stripe'; + * const billingGatewayService = new BillingGatewayService(provider); + */ +export class BillingGatewayService { + constructor(private readonly provider: z.infer) {} + + /** + * Creates a checkout session for billing. + * + * @param {CreateBillingCheckoutSchema} params - The parameters for creating the checkout session. + * + */ + async createCheckoutSession( + params: z.infer, + ) { + const strategy = await BillingGatewayFactoryService.GetProviderStrategy( + this.provider, + ); + + const payload = CreateBillingCheckoutSchema.parse(params); + + return strategy.createCheckoutSession(payload); + } + + /** + * Retrieves the checkout session from the specified provider. + * + * @param {RetrieveCheckoutSessionSchema} params - The parameters to retrieve the checkout session. + */ + async retrieveCheckoutSession( + params: z.infer, + ) { + const strategy = await BillingGatewayFactoryService.GetProviderStrategy( + this.provider, + ); + + const payload = RetrieveCheckoutSessionSchema.parse(params); + + return strategy.retrieveCheckoutSession(payload); + } + + /** + * Creates a billing portal session for the specified parameters. + * + * @param {CreateBillingPortalSessionSchema} params - The parameters to create the billing portal session. + */ + async createBillingPortalSession( + params: z.infer, + ) { + const strategy = await BillingGatewayFactoryService.GetProviderStrategy( + this.provider, + ); + + const payload = CreateBillingPortalSessionSchema.parse(params); + + return strategy.createBillingPortalSession(payload); + } + + /** + * Cancels a subscription. + * + * @param {CancelSubscriptionParamsSchema} params - The parameters for cancelling the subscription. + */ + async cancelSubscription( + params: z.infer, + ) { + const strategy = await BillingGatewayFactoryService.GetProviderStrategy( + this.provider, + ); + + const payload = CancelSubscriptionParamsSchema.parse(params); + + return strategy.cancelSubscription(payload); + } +} diff --git a/packages/billing-gateway/src/components/current-plan-card.tsx b/packages/billing-gateway/src/components/current-plan-card.tsx new file mode 100644 index 000000000..fa5a3987e --- /dev/null +++ b/packages/billing-gateway/src/components/current-plan-card.tsx @@ -0,0 +1 @@ +export function CurrentPlanCard(props: React.PropsWithChildren<{}>) {} diff --git a/packages/billing-gateway/src/components/embedded-checkout.tsx b/packages/billing-gateway/src/components/embedded-checkout.tsx new file mode 100644 index 000000000..afe209f14 --- /dev/null +++ b/packages/billing-gateway/src/components/embedded-checkout.tsx @@ -0,0 +1,39 @@ +import { lazy } from 'react'; + +import { Database } from '@kit/supabase/database'; + +type BillingProvider = Database['public']['Enums']['billing_provider']; + +export function EmbeddedCheckout( + props: React.PropsWithChildren<{ + checkoutToken: string; + provider: BillingProvider; + }>, +) { + const CheckoutComponent = loadCheckoutComponent(props.provider); + + return ; +} + +function loadCheckoutComponent(provider: BillingProvider) { + switch (provider) { + case 'stripe': { + return lazy(() => { + return import('@kit/stripe/components').then((c) => ({ + default: c.StripeCheckout, + })); + }); + } + + case 'lemon-squeezy': { + throw new Error('Lemon Squeezy is not yet supported'); + } + + case 'paddle': { + throw new Error('Paddle is not yet supported'); + } + + default: + throw new Error(`Unsupported provider: ${provider as string}`); + } +} diff --git a/packages/billing-gateway/src/components/index.ts b/packages/billing-gateway/src/components/index.ts new file mode 100644 index 000000000..67c25f1bf --- /dev/null +++ b/packages/billing-gateway/src/components/index.ts @@ -0,0 +1,3 @@ +export * from './plan-picker'; +export * from './current-plan-card'; +export * from './embedded-checkout'; diff --git a/packages/billing-gateway/src/components/plan-picker.tsx b/packages/billing-gateway/src/components/plan-picker.tsx new file mode 100644 index 000000000..463ffb895 --- /dev/null +++ b/packages/billing-gateway/src/components/plan-picker.tsx @@ -0,0 +1,222 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { ArrowRightIcon } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { BillingSchema } from '@kit/billing'; +import { Button } from '@kit/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@kit/ui/form'; +import { Label } from '@kit/ui/label'; +import { + RadioGroup, + RadioGroupItem, + RadioGroupItemLabel, +} from '@kit/ui/radio-group'; +import { Trans } from '@kit/ui/trans'; + +export function PlanPicker( + props: React.PropsWithChildren<{ + config: z.infer; + onSubmit: (data: { planId: string }) => void; + pending?: boolean; + }>, +) { + const intervals = props.config.products.reduce((acc, item) => { + return Array.from( + new Set([...acc, ...item.plans.map((plan) => plan.interval)]), + ); + }, []); + + const form = useForm({ + resolver: zodResolver( + z + .object({ + planId: z.string(), + interval: z.string(), + }) + .refine( + (data) => { + const planFound = props.config.products + .flatMap((item) => item.plans) + .some((plan) => plan.id === data.planId); + + if (!planFound) { + return false; + } + + return intervals.includes(data.interval); + }, + { message: 'Invalid plan', path: ['planId'] }, + ), + ), + defaultValues: { + interval: intervals[0], + planId: '', + }, + }); + + const selectedInterval = form.watch('interval'); + + return ( +
+ + { + return ( + + Choose your billing interval + + + + {intervals.map((interval) => { + return ( +
+ { + form.setValue('interval', interval); + }} + /> + + + + +
+ ); + })} +
+
+ +
+ ); + }} + /> + + ( + + Pick your preferred plan + + + + {props.config.products.map((item) => { + const variant = item.plans.find( + (plan) => plan.interval === selectedInterval, + ); + + if (!variant) { + throw new Error('No plan found'); + } + + return ( + + { + form.setValue('planId', variant.id); + }} + /> + +
+ + +
+
+ + + {formatCurrency( + item.currency.toLowerCase(), + variant.price, + )} + + +
+ +
+ + per {variant.interval} + +
+
+
+
+ ); + })} +
+
+ + +
+ )} + /> + +
+ +
+ + + ); +} + +function Price(props: React.PropsWithChildren) { + return ( + + {props.children} + + ); +} + +function formatCurrency(currencyCode: string, value: string) { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currencyCode, + }).format(value); +} diff --git a/packages/billing-gateway/src/gateway-provider-factory.ts b/packages/billing-gateway/src/gateway-provider-factory.ts new file mode 100644 index 000000000..bb67f06fd --- /dev/null +++ b/packages/billing-gateway/src/gateway-provider-factory.ts @@ -0,0 +1,33 @@ +import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; + +import { BillingGatewayService } from './billing-gateway-service'; + +/** + * @description This function retrieves the billing provider from the database and returns a + * new instance of the `BillingGatewayService` class. This class is used to interact with the server actions + * defined in the host application. + * @param {ReturnType} client - The Supabase server action client. + * + */ +export async function getGatewayProvider( + client: ReturnType, +) { + const provider = await getBillingProvider(client); + + return new BillingGatewayService(provider); +} + +async function getBillingProvider( + client: ReturnType, +) { + const { data, error } = await client + .from('config') + .select('billing_provider') + .single(); + + if (error ?? !data.billing_provider) { + throw error; + } + + return data.billing_provider; +} diff --git a/packages/billing-gateway/src/index.ts b/packages/billing-gateway/src/index.ts new file mode 100644 index 000000000..23c3dba8c --- /dev/null +++ b/packages/billing-gateway/src/index.ts @@ -0,0 +1,2 @@ +export * from './billing-gateway-service'; +export * from './gateway-provider-factory'; diff --git a/packages/billing-gateway/tsconfig.json b/packages/billing-gateway/tsconfig.json new file mode 100644 index 000000000..c4697e934 --- /dev/null +++ b/packages/billing-gateway/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@kit/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/billing/package.json b/packages/billing/package.json index 1881af686..d81b9670b 100644 --- a/packages/billing/package.json +++ b/packages/billing/package.json @@ -10,8 +10,9 @@ }, "prettier": "@kit/prettier-config", "exports": { - ".": "./src/create-billing-schema.ts", - "./components/*": "./src/components/*" + ".": "./src/index.ts", + "./components/*": "./src/components/*", + "./schema": "./src/schema/index.ts" }, "peerDependencies": { "react": "^18.2.0", diff --git a/packages/billing/src/create-billing-schema.ts b/packages/billing/src/create-billing-schema.ts index 00e426f6d..79a9c09be 100644 --- a/packages/billing/src/create-billing-schema.ts +++ b/packages/billing/src/create-billing-schema.ts @@ -2,7 +2,8 @@ import { z } from 'zod'; const Interval = z.enum(['month', 'year']); const PaymentType = z.enum(['recurring', 'one-time']); -const BillingProvider = z.enum(['stripe']); + +export const BillingProvider = z.enum(['stripe', 'paddle', 'lemon-squeezy']); const PlanSchema = z.object({ id: z.string().min(1), @@ -72,7 +73,6 @@ export function createBillingSchema(config: z.infer) { * Returns an array of billing plans based on the provided configuration. * * @param {Object} config - The configuration object containing product and plan information. - * @return {Array} - An array of billing plans. */ export function getBillingPlans(config: z.infer) { return config.products.flatMap((product) => product.plans); @@ -82,7 +82,6 @@ export function getBillingPlans(config: z.infer) { * Retrieves the intervals of all plans specified in the given configuration. * * @param {Object} config - The billing configuration containing products and plans. - * @returns {Array} - An array of intervals. */ export function getPlanIntervals(config: z.infer) { return Array.from( @@ -93,3 +92,18 @@ export function getPlanIntervals(config: z.infer) { ), ); } + +export function getProductPlanPairFromId( + config: z.infer, + planId: string, +) { + for (const product of config.products) { + const plan = product.plans.find((plan) => plan.id === planId); + + if (plan) { + return { product, plan }; + } + } + + return undefined; +} diff --git a/packages/billing/src/index.ts b/packages/billing/src/index.ts new file mode 100644 index 000000000..af37a5b62 --- /dev/null +++ b/packages/billing/src/index.ts @@ -0,0 +1,2 @@ +export * from './create-billing-schema'; +export * from './services/billing-strategy-provider.service'; diff --git a/packages/billing/src/schema/cancel-subscription-params.schema.ts b/packages/billing/src/schema/cancel-subscription-params.schema.ts new file mode 100644 index 000000000..b0e6ef48d --- /dev/null +++ b/packages/billing/src/schema/cancel-subscription-params.schema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export const CancelSubscriptionParamsSchema = z.object({ + subscriptionId: z.string(), + invoiceNow: z.boolean().optional(), +}); diff --git a/packages/billing/src/schema/create-biling-portal-session.schema.ts b/packages/billing/src/schema/create-biling-portal-session.schema.ts new file mode 100644 index 000000000..75affe124 --- /dev/null +++ b/packages/billing/src/schema/create-biling-portal-session.schema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export const CreateBillingPortalSessionSchema = z.object({ + returnUrl: z.string().url(), + customerId: z.string().min(1), +}); diff --git a/packages/billing/src/schema/create-billing-checkout.schema.ts b/packages/billing/src/schema/create-billing-checkout.schema.ts new file mode 100644 index 000000000..0f87c2e98 --- /dev/null +++ b/packages/billing/src/schema/create-billing-checkout.schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const CreateBillingCheckoutSchema = z.object({ + returnUrl: z.string().url(), + accountId: z.string(), + planId: z.string(), + paymentType: z.enum(['recurring', 'one-time']), + + trialPeriodDays: z.number().optional(), + + customerId: z.string().optional(), + customerEmail: z.string().optional(), +}); diff --git a/packages/billing/src/schema/index.ts b/packages/billing/src/schema/index.ts new file mode 100644 index 000000000..aa78ecdcb --- /dev/null +++ b/packages/billing/src/schema/index.ts @@ -0,0 +1,4 @@ +export * from './create-billing-checkout.schema'; +export * from './create-biling-portal-session.schema'; +export * from './retrieve-checkout-session.schema'; +export * from './cancel-subscription-params.schema'; diff --git a/packages/billing/src/schema/retrieve-checkout-session.schema.ts b/packages/billing/src/schema/retrieve-checkout-session.schema.ts new file mode 100644 index 000000000..4be18b3cf --- /dev/null +++ b/packages/billing/src/schema/retrieve-checkout-session.schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const RetrieveCheckoutSessionSchema = z.object({ + sessionId: z.string(), +}); diff --git a/packages/billing/src/services/billing-strategy-provider.service.ts b/packages/billing/src/services/billing-strategy-provider.service.ts new file mode 100644 index 000000000..63f500509 --- /dev/null +++ b/packages/billing/src/services/billing-strategy-provider.service.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +import { + CancelSubscriptionParamsSchema, + CreateBillingCheckoutSchema, + CreateBillingPortalSessionSchema, + RetrieveCheckoutSessionSchema, +} from '../schema'; + +export abstract class BillingStrategyProviderService { + abstract createBillingPortalSession( + params: z.infer, + ): Promise<{ + url: string; + }>; + + abstract retrieveCheckoutSession( + params: z.infer, + ): Promise; + + abstract createCheckoutSession( + params: z.infer, + ): Promise<{ + checkoutToken: string; + }>; + + abstract cancelSubscription( + params: z.infer, + ): Promise<{ + success: boolean; + }>; +} diff --git a/packages/stripe/package.json b/packages/stripe/package.json index 57bbf818a..17525a92a 100644 --- a/packages/stripe/package.json +++ b/packages/stripe/package.json @@ -11,10 +11,15 @@ }, "prettier": "@kit/prettier-config", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./components": "./src/components/index.ts" }, "dependencies": { - "stripe": "^14.21.0" + "stripe": "^14.21.0", + "@stripe/react-stripe-js": "^2.6.2", + "@stripe/stripe-js": "^3.0.10", + "@kit/billing": "0.1.0", + "@kit/ui": "0.1.0" }, "devDependencies": { "@kit/prettier-config": "0.1.0", diff --git a/packages/stripe/src/components/index.ts b/packages/stripe/src/components/index.ts new file mode 100644 index 000000000..57a128617 --- /dev/null +++ b/packages/stripe/src/components/index.ts @@ -0,0 +1 @@ +export * from './stripe-embedded-checkout'; diff --git a/packages/stripe/src/components/stripe-embedded-checkout.tsx b/packages/stripe/src/components/stripe-embedded-checkout.tsx new file mode 100644 index 000000000..763b02902 --- /dev/null +++ b/packages/stripe/src/components/stripe-embedded-checkout.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useState } from 'react'; + +import { + EmbeddedCheckout, + EmbeddedCheckoutProvider, +} from '@stripe/react-stripe-js'; +import { loadStripe } from '@stripe/stripe-js'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@kit/ui/dialog'; +import { cn } from '@kit/ui/utils'; + +const STRIPE_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY; + +if (!STRIPE_PUBLISHABLE_KEY) { + throw new Error( + 'Missing NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY environment variable. Did you forget to add it to your .env file?', + ); +} + +const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY); + +export function StripeCheckout({ + checkoutToken, + onClose, +}: React.PropsWithChildren<{ + checkoutToken: string; + onClose?: () => void; +}>) { + return ( + + + + + + ); +} + +function EmbeddedCheckoutPopup({ + onClose, + children, +}: React.PropsWithChildren<{ + onClose?: () => void; +}>) { + const [open, setOpen] = useState(true); + + const className = cn({ + [`bg-white p-4 max-h-[98vh] overflow-y-auto shadow-transparent border border-gray-200 dark:border-dark-700`]: + true, + }); + + return ( + { + if (!open && onClose) { + onClose(); + } + + setOpen(open); + }} + > + + Complete your purchase + + + e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + > +
{children}
+
+
+ ); +} diff --git a/packages/stripe/src/create-checkout.ts b/packages/stripe/src/create-checkout.ts index 2f215c525..f387325a1 100644 --- a/packages/stripe/src/create-checkout.ts +++ b/packages/stripe/src/create-checkout.ts @@ -1,14 +1,7 @@ import type { Stripe } from 'stripe'; +import { z } from 'zod'; -export interface CreateStripeCheckoutParams { - returnUrl: string; - organizationUid: string; - priceId: string; - customerId?: string; - trialPeriodDays?: number | undefined; - customerEmail?: string; - embedded: boolean; -} +import { CreateBillingCheckoutSchema } from '@kit/billing/schema'; /** * @name createStripeCheckout @@ -16,43 +9,43 @@ export interface CreateStripeCheckoutParams { * containing the session, which you can use to redirect the user to the * checkout page */ -export default async function createStripeCheckout( +export async function createStripeCheckout( stripe: Stripe, - params: CreateStripeCheckoutParams, + params: z.infer, ) { // in MakerKit, a subscription belongs to an organization, // rather than to a user // if you wish to change it, use the current user ID instead - const clientReferenceId = params.organizationUid; + const clientReferenceId = params.accountId; // we pass an optional customer ID, so we do not duplicate the Stripe // customers if an organization subscribes multiple times const customer = params.customerId ?? undefined; - // if it's a one-time payment - // you should change this to "payment" // docs: https://stripe.com/docs/billing/subscriptions/build-subscription - const mode: Stripe.Checkout.SessionCreateParams.Mode = 'subscription'; + const mode: Stripe.Checkout.SessionCreateParams.Mode = + params.paymentType === 'recurring' ? 'subscription' : 'payment'; + // TODO: support multiple line items and per-seat pricing const lineItem: Stripe.Checkout.SessionCreateParams.LineItem = { quantity: 1, - price: params.priceId, + price: params.planId, }; const subscriptionData: Stripe.Checkout.SessionCreateParams.SubscriptionData = { trial_period_days: params.trialPeriodDays, metadata: { - organizationUid: params.organizationUid, + accountId: params.accountId, }, }; const urls = getUrls({ - embedded: params.embedded, returnUrl: params.returnUrl, }); - const uiMode = params.embedded ? 'embedded' : 'hosted'; + // we use the embedded mode, so the user does not leave the page + const uiMode = 'embedded'; const customerData = customer ? { @@ -66,24 +59,17 @@ export default async function createStripeCheckout( mode, ui_mode: uiMode, line_items: [lineItem], - client_reference_id: clientReferenceId.toString(), + client_reference_id: clientReferenceId, subscription_data: subscriptionData, ...customerData, ...urls, }); } -function getUrls(params: { returnUrl: string; embedded?: boolean }) { - const successUrl = `${params.returnUrl}?success=true`; - const cancelUrl = `${params.returnUrl}?cancel=true`; - const returnUrl = `${params.returnUrl}/return?session_id={CHECKOUT_SESSION_ID}`; +function getUrls(params: { returnUrl: string }) { + const returnUrl = `${params.returnUrl}?session_id={CHECKOUT_SESSION_ID}`; - return params.embedded - ? { - return_url: returnUrl, - } - : { - success_url: successUrl, - cancel_url: cancelUrl, - }; + return { + return_url: returnUrl, + }; } diff --git a/packages/stripe/src/create-stripe-billing-portal-session.ts b/packages/stripe/src/create-stripe-billing-portal-session.ts index 3e00cfb0d..b25f464ae 100644 --- a/packages/stripe/src/create-stripe-billing-portal-session.ts +++ b/packages/stripe/src/create-stripe-billing-portal-session.ts @@ -1,9 +1,7 @@ import type { Stripe } from 'stripe'; +import { z } from 'zod'; -export interface CreateBillingPortalSessionParams { - customerId: string; - returnUrl: string; -} +import { CreateBillingPortalSessionSchema } from '@kit/billing/schema'; /** * @name createStripeBillingPortalSession @@ -11,7 +9,7 @@ export interface CreateBillingPortalSessionParams { */ export async function createStripeBillingPortalSession( stripe: Stripe, - params: CreateBillingPortalSessionParams, + params: z.infer, ) { return stripe.billingPortal.sessions.create({ customer: params.customerId, diff --git a/packages/stripe/src/index.ts b/packages/stripe/src/index.ts index e71f04376..754ff8968 100644 --- a/packages/stripe/src/index.ts +++ b/packages/stripe/src/index.ts @@ -1 +1 @@ -export { stripe } from './stripe.service'; +export { StripeBillingStrategyService } from './stripe.service'; diff --git a/packages/stripe/src/stripe.service.ts b/packages/stripe/src/stripe.service.ts index da22037c9..e3918c3a7 100644 --- a/packages/stripe/src/stripe.service.ts +++ b/packages/stripe/src/stripe.service.ts @@ -1,37 +1,59 @@ import 'server-only'; import type { Stripe } from 'stripe'; +import { z } from 'zod'; -import createStripeCheckout, { - CreateStripeCheckoutParams, -} from './create-checkout'; +import { BillingStrategyProviderService } from '@kit/billing'; import { - CreateBillingPortalSessionParams, - createStripeBillingPortalSession, -} from './create-stripe-billing-portal-session'; + CancelSubscriptionParamsSchema, + CreateBillingCheckoutSchema, + CreateBillingPortalSessionSchema, + RetrieveCheckoutSessionSchema, +} from '@kit/billing/schema'; + +import { createStripeCheckout } from './create-checkout'; +import { createStripeBillingPortalSession } from './create-stripe-billing-portal-session'; import { createStripeClient } from './stripe-sdk'; -class StripeService { - constructor(private readonly stripeProvider: () => Promise) {} - - async createCheckout(params: CreateStripeCheckoutParams) { +export class StripeBillingStrategyService + implements BillingStrategyProviderService +{ + async createCheckoutSession( + params: z.infer, + ) { const stripe = await this.stripeProvider(); return createStripeCheckout(stripe, params); } - async createBillingPortalSession(params: CreateBillingPortalSessionParams) { + async createBillingPortalSession( + params: z.infer, + ) { const stripe = await this.stripeProvider(); return createStripeBillingPortalSession(stripe, params); } - async cancelSubscription(subscriptionId: string) { + async cancelSubscription( + params: z.infer, + ) { const stripe = await this.stripeProvider(); - return stripe.subscriptions.cancel(subscriptionId, { - invoice_now: true, + await stripe.subscriptions.cancel(params.subscriptionId, { + invoice_now: params.invoiceNow ?? true, }); + + return { success: true }; + } + + async retrieveCheckoutSession( + params: z.infer, + ) { + const stripe = await this.stripeProvider(); + + return await stripe.subscriptions.retrieve(params.sessionId); + } + + private async stripeProvider(): Promise { + return createStripeClient(); } } - -export const stripe = new StripeService(createStripeClient); diff --git a/packages/supabase/src/clients/server-actions.client.ts b/packages/supabase/src/clients/server-actions.client.ts index 8b67eedd7..21604e021 100644 --- a/packages/supabase/src/clients/server-actions.client.ts +++ b/packages/supabase/src/clients/server-actions.client.ts @@ -6,19 +6,15 @@ import 'server-only'; import { Database } from '../database.types'; import { getSupabaseClientKeys } from '../get-supabase-client-keys'; -const createServerSupabaseClient = () => { +const createServerSupabaseClient = () => { const keys = getSupabaseClientKeys(); - return createServerClient(keys.url, keys.anonKey, { + return createServerClient(keys.url, keys.anonKey, { cookies: getCookiesStrategy(), }); }; -export const getSupabaseServerActionClient = < - GenericSchema = Database, ->(params?: { - admin: false; -}) => { +export const getSupabaseServerActionClient = (params?: { admin: false }) => { const keys = getSupabaseClientKeys(); const admin = params?.admin ?? false; @@ -35,7 +31,7 @@ export const getSupabaseServerActionClient = < throw new Error('Supabase Service Role Key not provided'); } - return createServerClient(keys.url, serviceRoleKey, { + return createServerClient(keys.url, serviceRoleKey, { auth: { persistSession: false, }, @@ -43,7 +39,7 @@ export const getSupabaseServerActionClient = < }); } - return createServerSupabaseClient(); + return createServerSupabaseClient(); }; function getCookiesStrategy() { diff --git a/packages/ui/README.md b/packages/ui/README.md index 9f77b8083..deeb6840c 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -5,4 +5,20 @@ This package is responsible for managing the UI components and styles across the This package define two sets of components: - `shadn-ui`: A set of UI components that can be used across the app using shadn UI -- `makerkit`: Components specific to MakerKit \ No newline at end of file +- `makerkit`: Components specific to MakerKit + +## Installing a Shadcn UI component + +To install a Shadcn UI component, you can use the following command in the root of the repository: + +```bash +npx shadcn-ui@latest add --path=packages/ui/src/shadcn +``` + +For example, to install the `Button` component, you can use the following command: + +```bash +npx shadcn-ui@latest add button --path=packages/ui/src/shadcn +``` + +We pass the `--path` flag to specify the path where the component should be installed. \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index a09494975..79028bebd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -25,6 +25,7 @@ "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-separator": "^1.0.3", "class-variance-authority": "^0.7.0", "react-top-loading-bar": "2.3.1", "clsx": "^2.1.0", @@ -91,6 +92,8 @@ "./heading": "./src/shadcn/heading.tsx", "./alert": "./src/shadcn/alert.tsx", "./badge": "./src/shadcn/badge.tsx", + "./radio-group": "./src/shadcn/radio-group.tsx", + "./separator": "./src/shadcn/separator.tsx", "./utils": "./src/utils/index.ts", "./if": "./src/makerkit/if.tsx", "./trans": "./src/makerkit/trans.tsx", diff --git a/packages/ui/src/shadcn/radio-group.tsx b/packages/ui/src/shadcn/radio-group.tsx index eb7150009..367c9ba29 100644 --- a/packages/ui/src/shadcn/radio-group.tsx +++ b/packages/ui/src/shadcn/radio-group.tsx @@ -42,4 +42,24 @@ const RadioGroupItem = React.forwardRef< }); RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; -export { RadioGroup, RadioGroupItem }; +const RadioGroupItemLabel = ( + props: React.PropsWithChildren<{ + className?: string; + }>, +) => { + return ( + + ); +}; +RadioGroupItemLabel.displayName = 'RadioGroupItemLabel'; + +export { RadioGroup, RadioGroupItem, RadioGroupItemLabel }; diff --git a/packages/ui/src/shadcn/separator.tsx b/packages/ui/src/shadcn/separator.tsx new file mode 100644 index 000000000..fa3a32749 --- /dev/null +++ b/packages/ui/src/shadcn/separator.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@kit/ui/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c167abe2..07c61365c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@kit/billing': specifier: ^0.1.0 version: link:../../packages/billing + '@kit/billing-gateway': + specifier: ^0.1.0 + version: link:../../packages/billing-gateway '@kit/emails': specifier: ^0.1.0 version: link:../../packages/emails @@ -215,6 +218,46 @@ importers: specifier: 0.1.0 version: link:../../tooling/typescript + packages/billing-gateway: + dependencies: + '@kit/billing': + specifier: 0.1.0 + version: link:../billing + '@kit/stripe': + specifier: 0.1.0 + version: link:../stripe + '@kit/supabase': + specifier: ^0.1.0 + version: link:../supabase + '@kit/ui': + specifier: 0.1.0 + version: link:../ui + lucide-react: + specifier: ^0.361.0 + version: 0.361.0(react@18.2.0) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + zod: + specifier: ^3.22.4 + version: 3.22.4 + devDependencies: + '@kit/eslint-config': + specifier: 0.2.0 + version: link:../../tooling/eslint + '@kit/prettier-config': + specifier: 0.1.0 + version: link:../../tooling/prettier + '@kit/tailwind-config': + specifier: 0.1.0 + version: link:../../tooling/tailwind + '@kit/tsconfig': + specifier: 0.1.0 + version: link:../../tooling/typescript + packages/emails: dependencies: '@react-email/components': @@ -434,6 +477,18 @@ importers: packages/stripe: dependencies: + '@kit/billing': + specifier: 0.1.0 + version: link:../billing + '@kit/ui': + specifier: 0.1.0 + version: link:../ui + '@stripe/react-stripe-js': + specifier: ^2.6.2 + version: 2.6.2(@stripe/stripe-js@3.0.10)(react-dom@18.2.0)(react@18.2.0) + '@stripe/stripe-js': + specifier: ^3.0.10 + version: 3.0.10 stripe: specifier: ^14.21.0 version: 14.21.0 @@ -517,6 +572,9 @@ importers: '@radix-ui/react-select': specifier: ^2.0.0 version: 2.0.0(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.67)(react@18.2.0) @@ -3141,6 +3199,27 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.67)(react@18.2.0) dev: false + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.67 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.0(react@18.2.0): resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==} peerDependencies: @@ -3741,6 +3820,24 @@ packages: resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} dev: false + /@stripe/react-stripe-js@2.6.2(@stripe/stripe-js@3.0.10)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-FSjNg4v7BiCfojvx25PQ8DugOa09cGk1t816R/DLI/lT+1bgRAYpMvoPirLT4ZQ3ev/0VDtPdWNaabPsLDTOMA==} + peerDependencies: + '@stripe/stripe-js': ^1.44.1 || ^2.0.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@stripe/stripe-js': 3.0.10 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@stripe/stripe-js@3.0.10: + resolution: {integrity: sha512-CFRNha+aPXR8GrqJss2TbK1j4aSGZXQY8gx0hvaYiSp+dU7EK/Zs5uwFTSAgV+t8H4+jcZ/iBGajAvoMYOwy+A==} + engines: {node: '>=12.16'} + dev: false + /@supabase/functions-js@2.1.5: resolution: {integrity: sha512-BNzC5XhCzzCaggJ8s53DP+WeHHGT/NfTsx2wUSSGKR2/ikLFQTBCDzMvGz/PxYMqRko/LwncQtKXGOYp1PkPaw==} dependencies: diff --git a/supabase/migrations/20221215192558_schema.sql b/supabase/migrations/20221215192558_schema.sql index b9c4509da..b5c8b13f4 100644 --- a/supabase/migrations/20221215192558_schema.sql +++ b/supabase/migrations/20221215192558_schema.sql @@ -141,10 +141,10 @@ create type public.billing_provider as ENUM('stripe', 'lemon-squeezy', 'paddle') */ create table if not exists public.config ( - enable_organization_accounts boolean default true, - enable_account_billing boolean default true, - enable_organization_billing boolean default true, - billing_provider public.billing_provider default 'stripe' + enable_organization_accounts boolean default true not null, + enable_account_billing boolean default true not null, + enable_organization_billing boolean default true not null, + billing_provider public.billing_provider default 'stripe' not null ); comment on table public.config is 'Configuration for the Supabase MakerKit.';