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 (
+
+
+ );
+}
+
+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 (
+
+ );
+}
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.';