diff --git a/apps/web/.env.development b/apps/web/.env.development
index 18b8608d1..943f5375f 100644
--- a/apps/web/.env.development
+++ b/apps/web/.env.development
@@ -15,7 +15,7 @@ NEXT_PUBLIC_AUTH_PASSWORD=true
NEXT_PUBLIC_AUTH_MAGIC_LINK=false
# BILLING
-NEXT_PUBLIC_BILLING_PROVIDER=stripe
+NEXT_PUBLIC_BILLING_PROVIDER=lemon-squeezy
# SUPABASE
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
diff --git a/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx b/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx
index 9f2927a16..af543ef5f 100644
--- a/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx
+++ b/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx
@@ -1,5 +1,5 @@
// We reuse the page from the billing module
// as there is no need to create a new one.
-import ReturnStripeSessionPage from '../../../[account]/billing/return/page';
+import ReturnCheckoutSessionPage from '../../../[account]/billing/return/page';
-export default ReturnStripeSessionPage;
+export default ReturnCheckoutSessionPage;
diff --git a/apps/web/app/(dashboard)/home/[account]/billing/return/page.tsx b/apps/web/app/(dashboard)/home/[account]/billing/return/page.tsx
index ef2ec0768..fccface0f 100644
--- a/apps/web/app/(dashboard)/home/[account]/billing/return/page.tsx
+++ b/apps/web/app/(dashboard)/home/[account]/billing/return/page.tsx
@@ -1,5 +1,5 @@
import dynamic from 'next/dynamic';
-import { notFound } from 'next/navigation';
+import { notFound, redirect } from 'next/navigation';
import { getBillingGatewayProvider } from '@kit/billing-gateway';
import { BillingSessionStatus } from '@kit/billing-gateway/components';
@@ -30,9 +30,13 @@ const LazyEmbeddedCheckout = dynamic(
);
async function ReturnCheckoutSessionPage({ searchParams }: SessionPageProps) {
- const { customerEmail, checkoutToken } = await loadCheckoutSession(
- searchParams.session_id,
- );
+ const sessionId = searchParams.session_id;
+
+ if (!sessionId) {
+ redirect('../');
+ }
+
+ const { customerEmail, checkoutToken } = await loadCheckoutSession(sessionId);
if (checkoutToken) {
return (
diff --git a/apps/web/public/locales/en/billing.json b/apps/web/public/locales/en/billing.json
index db87ed061..81f2ae83e 100644
--- a/apps/web/public/locales/en/billing.json
+++ b/apps/web/public/locales/en/billing.json
@@ -65,7 +65,7 @@
"canceled": {
"badge": "Canceled",
"heading": "Your subscription is canceled",
- "description": "Your subscription is canceled. It is scheduled to end on {{- endDate }}."
+ "description": "Your subscription is canceled. It is scheduled to end at end of the billing period."
},
"unpaid": {
"badge": "Unpaid",
diff --git a/packages/billing/gateway/src/components/current-subscription-card.tsx b/packages/billing/gateway/src/components/current-subscription-card.tsx
index d6fc0aefd..bc9449384 100644
--- a/packages/billing/gateway/src/components/current-subscription-card.tsx
+++ b/packages/billing/gateway/src/components/current-subscription-card.tsx
@@ -3,8 +3,6 @@ import { BadgeCheck } from 'lucide-react';
import { BillingConfig, getProductPlanPairByVariantId } from '@kit/billing';
import { Database } from '@kit/supabase/database';
-
-
import {
Card,
CardContent,
@@ -91,48 +89,44 @@ export function CurrentSubscriptionCard({
-
-
-
-
-
-
-
-
-
- {subscription.trial_ends_at
- ? formatDate(subscription.trial_ends_at, 'P')
- : ''}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {formatDate(subscription.period_ends_at ?? '', 'P')}
-
-
-
-
-
+
-
+
-
+
+
+ {subscription.trial_ends_at
+ ? formatDate(subscription.trial_ends_at, 'P')
+ : ''}
+
+
+
+
+
+
+
+
+
+
+
+ {formatDate(subscription.period_ends_at ?? '', 'P')}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/billing/gateway/src/components/embedded-checkout.tsx b/packages/billing/gateway/src/components/embedded-checkout.tsx
index 02466b229..b14f0876a 100644
--- a/packages/billing/gateway/src/components/embedded-checkout.tsx
+++ b/packages/billing/gateway/src/components/embedded-checkout.tsx
@@ -40,7 +40,15 @@ function loadCheckoutComponent(provider: BillingProvider) {
}
case 'lemon-squeezy': {
- throw new Error('Lemon Squeezy is not yet supported');
+ return buildLazyComponent(() => {
+ return import('@kit/lemon-squeezy/components').then(
+ ({ LemonSqueezyEmbeddedCheckout }) => {
+ return {
+ default: LemonSqueezyEmbeddedCheckout,
+ };
+ },
+ );
+ });
}
case 'paddle': {
diff --git a/packages/billing/lemon-squeezy/src/components/lemon-squeezy-embedded-checkout.tsx b/packages/billing/lemon-squeezy/src/components/lemon-squeezy-embedded-checkout.tsx
index 877bd46eb..88d2e385d 100644
--- a/packages/billing/lemon-squeezy/src/components/lemon-squeezy-embedded-checkout.tsx
+++ b/packages/billing/lemon-squeezy/src/components/lemon-squeezy-embedded-checkout.tsx
@@ -1,3 +1,7 @@
+'use client';
+
+import { useEffect } from 'react';
+
interface LemonSqueezyWindow extends Window {
createLemonSqueezy: () => void;
LemonSqueezy: {
@@ -14,16 +18,24 @@ interface LemonSqueezyWindow extends Window {
}
export function LemonSqueezyEmbeddedCheckout(props: { checkoutToken: string }) {
- return (
-
- );
+ return null;
+}
+
+function useLoadScript(checkoutToken: string) {
+ useEffect(() => {
+ const script = document.createElement('script');
+
+ script.src = 'https://app.lemonsqueezy.com/js/lemon.js';
+
+ script.onload = () => {
+ const win = window as unknown as LemonSqueezyWindow;
+
+ win.createLemonSqueezy();
+ win.LemonSqueezy.Url.Open(checkoutToken);
+ };
+
+ document.body.appendChild(script);
+ }, [checkoutToken]);
}
diff --git a/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-checkout.ts b/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-checkout.ts
index a553f634b..62b32c24c 100644
--- a/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-checkout.ts
+++ b/packages/billing/lemon-squeezy/src/services/create-lemon-squeezy-checkout.ts
@@ -29,12 +29,9 @@ export async function createLemonSqueezyCheckout(
}
const env = getLemonSqueezyEnv();
- const storeId = env.storeId;
- const variantId = lineItem.id;
- const urls = getUrls({
- returnUrl: params.returnUrl,
- });
+ const storeId = Number(env.storeId);
+ const variantId = Number(lineItem.id);
const customer = params.customerId
? await getCustomer(params.customerId)
@@ -63,7 +60,7 @@ export async function createLemonSqueezyCheckout(
},
},
productOptions: {
- redirectUrl: urls.return_url,
+ redirectUrl: params.returnUrl,
},
expiresAt: null,
preview: true,
@@ -72,11 +69,3 @@ export async function createLemonSqueezyCheckout(
return createCheckout(storeId, variantId, newCheckout);
}
-
-function getUrls(params: { returnUrl: string }) {
- const returnUrl = `${params.returnUrl}?session_id={CHECKOUT_SESSION_ID}`;
-
- return {
- return_url: returnUrl,
- };
-}
diff --git a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts
index cdf5d8910..d898aacf5 100644
--- a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts
+++ b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts
@@ -32,7 +32,6 @@ export class LemonSqueezyBillingStrategyService
accountId: params.accountId,
returnUrl: params.returnUrl,
trialDays: params.trialDays,
- planId: params.plan.id,
},
'Creating checkout session...',
);
@@ -40,6 +39,8 @@ export class LemonSqueezyBillingStrategyService
const { data: response, error } = await createLemonSqueezyCheckout(params);
if (error ?? !response?.data.id) {
+ console.log(error);
+
Logger.error(
{
name: 'billing.lemon-squeezy',
@@ -62,7 +63,9 @@ export class LemonSqueezyBillingStrategyService
'Checkout session created successfully',
);
- return { checkoutToken: response.data.id };
+ return {
+ checkoutToken: response.data.attributes.url,
+ };
}
async createBillingPortalSession(
diff --git a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-webhook-handler.service.ts b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-webhook-handler.service.ts
index 5efe8443f..fd3b0e3e4 100644
--- a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-webhook-handler.service.ts
+++ b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-webhook-handler.service.ts
@@ -296,6 +296,8 @@ export class LemonSqueezyWebhookHandlerService
return {
id: item.id,
quantity,
+ interval: params.interval,
+ interval_count: params.intervalCount,
subscription_id: params.id,
product_id: item.product,
variant_id: item.variant,
@@ -317,8 +319,12 @@ export class LemonSqueezyWebhookHandlerService
cancel_at_period_end: params.cancelAtPeriodEnd ?? false,
period_starts_at: getISOString(params.periodStartsAt) as string,
period_ends_at: getISOString(params.periodEndsAt) as string,
- trial_starts_at: getISOString(params.trialStartsAt),
- trial_ends_at: getISOString(params.trialEndsAt),
+ trial_starts_at: params.trialStartsAt
+ ? getISOString(params.trialStartsAt)
+ : undefined,
+ trial_ends_at: params.trialEndsAt
+ ? getISOString(params.trialEndsAt)
+ : undefined,
};
}
@@ -360,7 +366,7 @@ export class LemonSqueezyWebhookHandlerService
}
function getISOString(date: number | null) {
- return date ? new Date(date * 1000).toISOString() : undefined;
+ return date ? new Date(date).toISOString() : undefined;
}
function isSigningSecretValid(rawBody: Buffer, signatureHeader: string) {
diff --git a/supabase/migrations/20221215192558_schema.sql b/supabase/migrations/20221215192558_schema.sql
index ec571e24a..cdfc96d93 100644
--- a/supabase/migrations/20221215192558_schema.sql
+++ b/supabase/migrations/20221215192558_schema.sql
@@ -126,7 +126,8 @@ create type public.billing_provider as ENUM(
create table if not exists public.config(
enable_team_accounts boolean default true not null,
enable_account_billing boolean default true not null,
- enable_team_account_billing boolean default true not null
+ enable_team_account_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.';
@@ -137,6 +138,8 @@ comment on column public.config.enable_account_billing is 'Enable billing for in
comment on column public.config.enable_team_account_billing is 'Enable billing for team accounts';
+comment on column public.config.billing_provider is 'The billing provider to use';
+
alter table public.config enable row level security;
-- create config row
@@ -1101,8 +1104,6 @@ comment on column public.subscriptions.currency is 'The currency for the subscri
comment on column public.subscriptions.status is 'The status of the subscription';
-comment on column public.subscriptions.type is 'The type of the subscription, either one-off or recurring';
-
comment on column public.subscriptions.period_starts_at is 'The start of the current period for the subscription';
comment on column public.subscriptions.period_ends_at is 'The end of the current period for the subscription';
@@ -1128,8 +1129,8 @@ alter table public.subscriptions enable row level security;
create policy subscriptions_read_self on public.subscriptions
for select to authenticated
using (
- (has_role_on_account(account_id) and config.is_set('enable_team_account_billing'))
- or (account_id = auth.uid() and config.is_set('enable_account_billing'))
+ (has_role_on_account(account_id) and public.is_set('enable_team_account_billing'))
+ or (account_id = auth.uid() and public.is_set('enable_account_billing'))
);
-- Functions
@@ -1140,8 +1141,7 @@ create or replace function
public.billing_provider, cancel_at_period_end bool, currency
varchar(3), period_starts_at timestamptz, period_ends_at
timestamptz, line_items jsonb, trial_starts_at timestamptz
- default null, trial_ends_at timestamptz default null, type
- public.subscription_type default 'recurring')
+ default null, trial_ends_at timestamptz default null)
returns public.subscriptions
as $$
declare
@@ -1171,7 +1171,6 @@ on conflict (
id,
active,
status,
- type,
billing_provider,
cancel_at_period_end,
currency,
@@ -1182,10 +1181,9 @@ on conflict (
values (
target_account_id,
new_billing_customer_id,
- subscription_id,
+ target_subscription_id,
active,
status,
- type,
billing_provider,
cancel_at_period_end,
currency,
@@ -1226,7 +1224,7 @@ on conflict (
interval,
interval_count)
select
- subscription_id,
+ target_subscription_id,
prod_id,
var_id,
price_amt,
@@ -1254,7 +1252,7 @@ language plpgsql;
grant execute on function public.upsert_subscription(uuid, varchar,
text, bool, public.subscription_status, public.billing_provider,
bool, varchar, timestamptz, timestamptz, jsonb, timestamptz,
- timestamptz, public.subscription_type) to service_role;
+ timestamptz) to service_role;
/* -------------------------------------------------------
@@ -1356,8 +1354,8 @@ alter table public.orders enable row level security;
-- account is their own
create policy orders_read_self on public.orders
for select to authenticated
- using ((account_id = auth.uid() and config.is_set('enable_account_billing'))
- or (has_role_on_account(account_id) and config.is_set('enable_team_account_billing')));
+ using ((account_id = auth.uid() and public.is_set('enable_account_billing'))
+ or (has_role_on_account(account_id) and public.is_set('enable_team_account_billing')));
/**