-
-
Notifications
You must be signed in to change notification settings - Fork 19
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
Add payment system #3094
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 b5d5494
update: initial ui for billing page
phatgg221 586553d
fix: dark mode and light mode color
phatgg221 1d35009
feat: ui update and polar product chosen
phatgg221 ed59b27
chore: polar.sh setting up [bugs]
phatgg221 bd9bd09
Update data-polar-checkout.tsx
phatgg221 a0e1d59
fix: changed to sandbox
phatgg221 1d157ad
fix: sandbox
phatgg221 50ce46c
refactor: refactor checkout route
phatgg221 627cac5
chore: delete unwanted history
phatgg221 3fa806e
Merge branch 'main' into feat/payment-methods
phatgg221 35fcb79
chore: adding webhooks polar
phatgg221 c848591
fix: using temp payment
phatgg221 1def439
feat: update success page
phatgg221 d45c8e4
feat: refactor dodo payment prodcuts
phatgg221 265c72b
feat: moving to Dodo payments
phatgg221 42ddcf6
chore: update dodo payment legacy
phatgg221 a05641a
refactor: refactor to polar.sh
phatgg221 df81f1b
chore: delete unwanted comments
phatgg221 cec2ac1
fix!: wrong webhooks code
phatgg221 c6ede86
feat: update subscription on Database
phatgg221 343b94d
Merge branch 'main' into feat/payment-methods
phatgg221 9504866
update: current plan
phatgg221 19d0f42
fix: update real product
phatgg221 40c972e
update: success page real data
phatgg221 cbc5244
Merge branch 'main' into feat/payment-methods
phatgg221 e17ec6d
feat: update allow user to buy subscription under ceritain condition
phatgg221 678d933
refactor: fetched data concurrently
phatgg221 0aba322
feat: adding usage billing for users per workspace
phatgg221 2e31f81
Merge branch 'main' into feat/payment-methods
phatgg221 38b8fcc
fix: fix builds
phatgg221 32bfbbb
Merge branch 'main' into feat/payment-methods
vhpx 7fcb1ee
chore: ran prettier format
vhpx 32f7ae2
feat(db): add workspace subscription table and related policies (cons…
vhpx 9134f7d
chore(db): reorder arguments in database function types for consistency
vhpx aadb5d0
feat(payment): integrate @tuturuuu/payment package and update referen…
vhpx eb3be06
refactor(billing): update PurchaseLink component to use dynamic custo…
vhpx 8c78ada
feat(billing): enforce root workspace and member requirements for bil…
vhpx d0b9869
chore(db): reorder and update arguments in database function types fo…
vhpx 16b3053
fix(db): update workspace subscription policy to allow inserts for ow…
vhpx fcbf04f
Merge branch 'main' into feat/payment-methods
vhpx aa5c52a
chore(db): consolidate database schema
vhpx b4c7261
fix(db): ensure all migrations are applied
vhpx 95cde62
Merge branch 'main' into feat/payment-methods
vhpx 997ea7c
fix(build): use fixed product id for polar
vhpx 6a2ba48
fix(db): set default value for services column in users table
vhpx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
104 changes: 104 additions & 0 deletions
104
apps/db/supabase/migrations/20250616064232_add_workspace_subscriptions.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
vhpx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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"; | ||
vhpx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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)))); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
237 changes: 237 additions & 0 deletions
237
apps/web/src/app/[locale]/(dashboard)/[wsId]/billing/billing-client.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
))} | ||
phatgg221 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</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> | ||
vhpx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</div> | ||
</div> | ||
<div className="mt-6"> | ||
<Button | ||
variant="outline" | ||
size="sm" | ||
className="text-sm font-medium" | ||
> | ||
Update payment method | ||
</Button> | ||
vhpx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</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> | ||
))} | ||
vhpx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</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> | ||
)} | ||
</> | ||
); | ||
} | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.