Skip to content

Add payment system #3094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
375a106
feat: add billing to sidebar
phatgg221 Jun 13, 2025
b5d5494
update: initial ui for billing page
phatgg221 Jun 13, 2025
586553d
fix: dark mode and light mode color
phatgg221 Jun 13, 2025
1d35009
feat: ui update and polar product chosen
phatgg221 Jun 13, 2025
ed59b27
chore: polar.sh setting up [bugs]
phatgg221 Jun 13, 2025
bd9bd09
Update data-polar-checkout.tsx
phatgg221 Jun 13, 2025
a0e1d59
fix: changed to sandbox
phatgg221 Jun 13, 2025
1d157ad
fix: sandbox
phatgg221 Jun 13, 2025
50ce46c
refactor: refactor checkout route
phatgg221 Jun 13, 2025
627cac5
chore: delete unwanted history
phatgg221 Jun 13, 2025
3fa806e
Merge branch 'main' into feat/payment-methods
phatgg221 Jun 13, 2025
35fcb79
chore: adding webhooks polar
phatgg221 Jun 14, 2025
c848591
fix: using temp payment
phatgg221 Jun 14, 2025
1def439
feat: update success page
phatgg221 Jun 15, 2025
d45c8e4
feat: refactor dodo payment prodcuts
phatgg221 Jun 15, 2025
265c72b
feat: moving to Dodo payments
phatgg221 Jun 15, 2025
42ddcf6
chore: update dodo payment legacy
phatgg221 Jun 15, 2025
a05641a
refactor: refactor to polar.sh
phatgg221 Jun 16, 2025
df81f1b
chore: delete unwanted comments
phatgg221 Jun 16, 2025
cec2ac1
fix!: wrong webhooks code
phatgg221 Jun 16, 2025
c6ede86
feat: update subscription on Database
phatgg221 Jun 16, 2025
343b94d
Merge branch 'main' into feat/payment-methods
phatgg221 Jun 16, 2025
9504866
update: current plan
phatgg221 Jun 16, 2025
19d0f42
fix: update real product
phatgg221 Jun 16, 2025
40c972e
update: success page real data
phatgg221 Jun 16, 2025
cbc5244
Merge branch 'main' into feat/payment-methods
phatgg221 Jun 16, 2025
e17ec6d
feat: update allow user to buy subscription under ceritain condition
phatgg221 Jun 17, 2025
678d933
refactor: fetched data concurrently
phatgg221 Jun 17, 2025
0aba322
feat: adding usage billing for users per workspace
phatgg221 Jun 17, 2025
2e31f81
Merge branch 'main' into feat/payment-methods
phatgg221 Jun 17, 2025
38b8fcc
fix: fix builds
phatgg221 Jun 17, 2025
32bfbbb
Merge branch 'main' into feat/payment-methods
vhpx Jun 17, 2025
7fcb1ee
chore: ran prettier format
vhpx Jun 17, 2025
32f7ae2
feat(db): add workspace subscription table and related policies (cons…
vhpx Jun 17, 2025
9134f7d
chore(db): reorder arguments in database function types for consistency
vhpx Jun 17, 2025
aadb5d0
feat(payment): integrate @tuturuuu/payment package and update referen…
vhpx Jun 17, 2025
eb3be06
refactor(billing): update PurchaseLink component to use dynamic custo…
vhpx Jun 17, 2025
8c78ada
feat(billing): enforce root workspace and member requirements for bil…
vhpx Jun 17, 2025
d0b9869
chore(db): reorder and update arguments in database function types fo…
vhpx Jun 17, 2025
16b3053
fix(db): update workspace subscription policy to allow inserts for ow…
vhpx Jun 17, 2025
fcbf04f
Merge branch 'main' into feat/payment-methods
vhpx Jun 17, 2025
aa5c52a
chore(db): consolidate database schema
vhpx Jun 17, 2025
b4c7261
fix(db): ensure all migrations are applied
vhpx Jun 17, 2025
95cde62
Merge branch 'main' into feat/payment-methods
vhpx Jun 17, 2025
997ea7c
fix(build): use fixed product id for polar
vhpx Jun 17, 2025
6a2ba48
fix(db): set default value for services column in users table
vhpx Jun 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
create type "public"."subscription_status" as enum ('trialing', 'active', 'canceled', 'past_due');

create table "public"."workspace_subscription" (
"id" uuid not null default gen_random_uuid(),
"created_at" timestamp with time zone not null default now(),
"ws_id" uuid default gen_random_uuid(),
"status" subscription_status,
"polar_subscription_id" text not null,
"product_id" uuid default gen_random_uuid(),
"price_id" uuid default gen_random_uuid(),
"current_period_start" timestamp with time zone,
"current_period_end" timestamp with time zone,
"cancel_at_period_end" boolean default false,
"updated_at" timestamp with time zone default now()
);


alter table "public"."workspace_subscription" enable row level security;

CREATE UNIQUE INDEX workspace_subscription_pkey ON public.workspace_subscription USING btree (id, polar_subscription_id);

alter table "public"."workspace_subscription" add constraint "workspace_subscription_pkey" PRIMARY KEY using index "workspace_subscription_pkey";

alter table "public"."workspace_subscription" add constraint "workspace_subscription_ws_id_fkey" FOREIGN KEY (ws_id) REFERENCES workspaces(id) ON DELETE CASCADE not valid;

alter table "public"."workspace_subscription" validate constraint "workspace_subscription_ws_id_fkey";

grant delete on table "public"."workspace_subscription" to "anon";

grant insert on table "public"."workspace_subscription" to "anon";

grant references on table "public"."workspace_subscription" to "anon";

grant select on table "public"."workspace_subscription" to "anon";

grant trigger on table "public"."workspace_subscription" to "anon";

grant truncate on table "public"."workspace_subscription" to "anon";

grant update on table "public"."workspace_subscription" to "anon";

grant delete on table "public"."workspace_subscription" to "authenticated";

grant insert on table "public"."workspace_subscription" to "authenticated";

grant references on table "public"."workspace_subscription" to "authenticated";

grant select on table "public"."workspace_subscription" to "authenticated";

grant trigger on table "public"."workspace_subscription" to "authenticated";

grant truncate on table "public"."workspace_subscription" to "authenticated";

grant update on table "public"."workspace_subscription" to "authenticated";

grant delete on table "public"."workspace_subscription" to "service_role";

grant insert on table "public"."workspace_subscription" to "service_role";

grant references on table "public"."workspace_subscription" to "service_role";

grant select on table "public"."workspace_subscription" to "service_role";

grant trigger on table "public"."workspace_subscription" to "service_role";

grant truncate on table "public"."workspace_subscription" to "service_role";

grant update on table "public"."workspace_subscription" to "service_role";

set check_function_bodies = off;

CREATE OR REPLACE FUNCTION public.check_ws_creator(ws_id uuid)
RETURNS boolean
LANGUAGE plpgsql
AS $function$BEGIN
RETURN (
(SELECT creator_id FROM public.workspaces WHERE id = ws_id) = auth.uid()

AND NOT EXISTS (
SELECT 1 FROM public.workspace_subscription
WHERE public.workspace_subscription.ws_id = ws_id
);
)
END$function$
;

create policy "allow select for users that are in the workspace"
on "public"."workspace_subscription"
as permissive
for select
to authenticated
using ((auth.uid() = ( SELECT workspaces.creator_id
FROM workspaces
WHERE (workspaces.id = workspace_subscription.ws_id))));


create policy "only allow owner of the user to buy subscription"
on "public"."workspace_subscription"
as permissive
for insert
to public
using ((auth.uid() = ( SELECT workspaces.creator_id
FROM workspaces
WHERE (workspaces.id = workspace_subscription.ws_id))));
6 changes: 5 additions & 1 deletion apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ SCRAPER_URL=YOUR_SCRAPER_URL
AURORA_EXTERNAL_URL=YOUR_AURORA_EXTERNAL_URL
AURORA_EXTERNAL_WSID=YOUR_AURORA_EXTERNAL_WSID
PROXY_API_KEY=YOUR_PROXY_API_KEY
NEXT_PUBLIC_PROXY_API_KEY=YOUR_NEXT_PUBLIC_PROXY_API_KEY
NEXT_PUBLIC_PROXY_API_KEY=YOUR_NEXT_PUBLIC_PROXY_API_KEY

# Payment Polar.sh
POLAR_WEBHOOK_SECRET=YOUR_POLAR_WEBHOOK_SECRET
NEXT_PUBLIC_POLAR_ACCESS_TOKEN=YOUR_NEXT_PUBLIC_POLAR_ACCESS_TOKEN
1 change: 1 addition & 0 deletions apps/web/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@
"migrations": "Migrations",
"notifications": "Notifications",
"new-ws": "New workspace",
"billing": "Billing",
"new-team": "New Team",
"new-task": "New Task",
"new-note": "New Note",
Expand Down
1 change: 1 addition & 0 deletions apps/web/messages/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@
"migrations": "Chuyển dữ liệu",
"notifications": "Thông báo",
"new-ws": "Tạo tổ chức mới",
"billing": "Thanh toán",
"new-team": "Tạo nhóm mới",
"new-task": "Tạo công việc mới",
"new-note": "Tạo ghi chú mới",
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@tuturuuu/auth": "workspace:*",
"@tuturuuu/supabase": "workspace:*",
"@tuturuuu/transactional": "workspace:*",
"@tuturuuu/payment": "workspace:*",
"@tuturuuu/types": "workspace:*",
"@tuturuuu/ui": "workspace:*",
"@tuturuuu/utils": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
'use client';

import PurchaseLink from './data-polar-checkout';
import { Button } from '@tuturuuu/ui/button';
import { ArrowUpCircle, CheckCircle, CreditCard } from 'lucide-react';
import { useState } from 'react';

// Define types for the props we're passing from the server component
interface Plan {
name: string;
price: string;
billingCycle: string;
startDate?: string;
nextBillingDate?: string;
status?: string;
features?: string[];
}

interface UpgradePlan {
id: string;
name: string;
price: string;
billingCycle: string;
popular: boolean;
features: string[];
isEnterprise?: boolean;
}

interface BillingClientProps {
currentPlan: Plan;
upgradePlans: UpgradePlan[];
wsId: string;
isCreator: boolean;
}

export function BillingClient({
currentPlan,
upgradePlans,
wsId,
isCreator,
}: BillingClientProps) {
const [showUpgradeOptions, setShowUpgradeOptions] = useState(false);

return (
<>
{/* Current Plan Card */}
<div className="mb-8 rounded-lg border border-border bg-card p-8 shadow-sm dark:bg-card/80">
<div className="mb-6 flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold tracking-tight text-card-foreground">
Current Plan
</h2>
<p className="text-muted-foreground">Your subscription details</p>
</div>
<div className="flex items-center">
<span
className={`rounded-full px-3 py-1 text-sm font-medium ${
currentPlan.status === 'active'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'
}`}
>
{currentPlan.status === 'active' ? 'Active' : 'Pending'}
</span>
</div>
</div>

<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<div className="col-span-2">
<div className="mb-6">
<h3 className="mb-1 text-xl font-bold text-card-foreground">
{currentPlan.name}
</h3>
<p className="text-2xl font-bold text-primary">
{currentPlan.price}
<span className="text-sm text-muted-foreground">
/{currentPlan.billingCycle}
</span>
</p>
</div>

<div className="mb-6 grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Start Date</p>
<p className="font-medium text-card-foreground">
{currentPlan.startDate}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">
Next Billing Date
</p>
<p className="font-medium text-card-foreground">
{currentPlan.nextBillingDate}
</p>
</div>
</div>

<div className="mb-6">
<h4 className="mb-2 font-medium text-card-foreground">
Features:
</h4>
<ul className="grid grid-cols-1 gap-2 md:grid-cols-2">
{currentPlan.features?.map((feature, index) => (
<li
key={index}
className="flex items-center text-card-foreground"
>
<CheckCircle className="mr-2 h-5 w-5 text-primary" />
{feature}
</li>
))}
</ul>
</div>

<div className="mt-6 flex flex-wrap gap-3">
<Button
disabled={!isCreator}
onClick={() => setShowUpgradeOptions(!showUpgradeOptions)}
className="flex items-center"
>
<ArrowUpCircle className="mr-1 h-5 w-5" />
{showUpgradeOptions ? 'Hide Options' : 'Upgrade Plan'}
</Button>
<Button variant="outline" className="border-border">
Cancel Subscription
</Button>
</div>
</div>

<div className="rounded-lg border border-border bg-accent/30 p-6">
<h3 className="mb-4 text-lg font-semibold text-card-foreground">
Payment Method
</h3>
<div className="mb-4 flex items-center">
<CreditCard className="mr-3 h-8 w-8 text-muted-foreground" />
<div>
<p className="font-medium text-card-foreground">
Visa ending in 4242
</p>
<p className="text-sm text-muted-foreground">Expires 05/2025</p>
</div>
</div>
<div className="mt-6">
<Button
variant="outline"
size="sm"
className="text-sm font-medium"
>
Update payment method
</Button>
</div>
</div>
</div>
</div>

{/* Upgrade Options */}
{showUpgradeOptions && (
<div className="mb-8 rounded-lg border-2 border-primary/20 bg-card p-8 shadow-sm dark:bg-card/80">
<h2 className="mb-6 text-2xl font-semibold text-card-foreground">
Upgrade Options
</h2>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{upgradePlans.map((plan) => (
<div
key={plan.id}
className={`rounded-lg border transition-shadow hover:shadow-md ${
plan.popular ? 'relative border-primary' : 'border-border'
}`}
>
{plan.popular && (
<div className="absolute top-0 right-0 rounded-tr-md rounded-bl-lg bg-primary px-3 py-1 text-xs text-primary-foreground">
RECOMMENDED
</div>
)}
<div className="p-6">
<h3 className="mb-1 text-xl font-bold text-card-foreground">
{plan.name}
</h3>
<p className="mb-4 text-2xl font-bold text-primary">
{plan.price}
<span className="text-sm text-muted-foreground">
/{plan.billingCycle}
</span>
</p>
<ul className="mb-6">
{plan.features.map((feature, index) => (
<li key={index} className="mb-2 flex items-start">
<CheckCircle className="mt-0.5 mr-2 h-5 w-5 flex-shrink-0 text-primary" />
<span className="text-card-foreground">{feature}</span>
</li>
))}
</ul>
{plan.isEnterprise ? (
<Button className="w-full" variant="outline" disabled>
Contact Sales
</Button>
) : (
<Button
variant={plan.popular ? 'default' : 'outline'}
className={`w-full ${
plan.popular
? ''
: 'border-primary bg-transparent text-primary hover:bg-primary/10'
}`}
asChild
>
<PurchaseLink
productId={plan.id}
wsId={wsId}
customerEmail="t@test.com"
theme="auto"
className="flex w-full items-center justify-center"
>
Select {plan.name}
</PurchaseLink>
</Button>
)}
{plan.isEnterprise && (
<p className="mt-2 text-center text-xs text-muted-foreground">
Please contact our sales team for Enterprise pricing
</p>
)}
</div>
</div>
))}
</div>
<p className="mt-6 text-sm text-muted-foreground">
* Upgrading your plan will take effect immediately. You'll be
charged the prorated amount for the remainder of your current
billing cycle.
</p>
</div>
)}
</>
);
}

Check warning on line 237 in apps/web/src/app/[locale]/(dashboard)/[wsId]/billing/billing-client.tsx

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/[locale]/(dashboard)/[wsId]/billing/billing-client.tsx#L2-L237

Added lines #L2 - L237 were not covered by tests
Loading