Skip to content

Commit b9ec7d8

Browse files
committed
feat: Enhance subscription management with improved customer handling and checkout session creation
1 parent 6524cea commit b9ec7d8

File tree

8 files changed

+517
-154
lines changed

8 files changed

+517
-154
lines changed

app/src/hooks.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ async function getSession(event: RequestEvent) {
1616
}
1717

1818
export const handle: Handle = async ({ event, resolve }) => {
19+
// Ignore Chrome DevTools special paths to prevent 404 errors in logs
20+
if (event.url.pathname.startsWith("/.well-known/appspecific")) {
21+
return new Response(null, { status: 200 });
22+
}
23+
1924
// Set the session in locals for server routes to access
2025
event.locals.session = await getSession(event);
2126

app/src/lib/server/env.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { env } from "$env/dynamic/private";
7+
import { PUBLIC_SUPABASE_URL } from "$env/static/public";
78

89
// The server-side environment variables
910
// Using platform's env system for private variables first, then import.meta.env as fallback
@@ -22,11 +23,7 @@ export const SUPABASE_SERVICE_ROLE_KEY =
2223
import.meta.env.SUPABASE_SERVICE_ROLE_KEY ||
2324
import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY ||
2425
"";
25-
export const SUPABASE_URL =
26-
env.SUPABASE_URL ||
27-
import.meta.env.SUPABASE_URL ||
28-
import.meta.env.VITE_SUPABASE_URL ||
29-
"";
26+
export const SUPABASE_URL = PUBLIC_SUPABASE_URL;
3027

3128
// Function to check if all required environment variables are set
3229
export function validateEnv() {

app/src/lib/server/subscription.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* Server-side subscription helpers
3+
*
4+
* This module provides functions for managing Stripe subscriptions on the server side.
5+
*/
6+
import { stripe, isStripeConfigured } from "$lib/server/stripe";
7+
import { pricingPlans } from "$lib/subscription/pricing-plans";
8+
import type { User } from "@supabase/supabase-js";
9+
import type { SupabaseClient } from "@supabase/supabase-js";
10+
import type { Database } from "$lib/database/supabase-types";
11+
12+
/**
13+
* Gets an existing Stripe customer ID for a user, or creates one if it doesn't exist
14+
*/
15+
export async function getOrCreateCustomerId({
16+
supabaseAdmin,
17+
user,
18+
}: {
19+
supabaseAdmin: SupabaseClient<Database>;
20+
user: User;
21+
}): Promise<{ customerId?: string; error?: any }> {
22+
try {
23+
// Check if the user already has a Stripe customer ID
24+
const { data: existingCustomer, error: fetchError } = await supabaseAdmin
25+
.from("stripe_customers")
26+
.select("stripe_customer_id")
27+
.eq("user_id", user.id)
28+
.single();
29+
30+
if (fetchError && fetchError.code !== "PGRST116") {
31+
// PGRST116 = not found
32+
return { error: fetchError };
33+
}
34+
35+
if (existingCustomer?.stripe_customer_id) {
36+
return { customerId: existingCustomer.stripe_customer_id };
37+
}
38+
39+
// Fetch profile data needed to create customer
40+
const { data: profile, error: profileError } = await supabaseAdmin
41+
.from("profiles")
42+
.select("full_name, website")
43+
.eq("id", user.id)
44+
.single();
45+
46+
if (profileError) {
47+
return { error: profileError };
48+
}
49+
50+
// Create a stripe customer
51+
if (!isStripeConfigured()) {
52+
return { error: "Stripe is not properly configured" };
53+
}
54+
55+
try {
56+
const customer = await stripe.customers.create({
57+
email: user.email,
58+
name: profile.full_name || user.email || "",
59+
metadata: {
60+
user_id: user.id,
61+
website: profile.website || "",
62+
},
63+
});
64+
65+
if (!customer.id) {
66+
return { error: "Unknown Stripe customer creation error" };
67+
}
68+
69+
// Save the customer ID in our database
70+
const { error: insertError } = await supabaseAdmin
71+
.from("stripe_customers")
72+
.insert({
73+
user_id: user.id,
74+
stripe_customer_id: customer.id,
75+
updated_at: new Date().toISOString(),
76+
});
77+
78+
if (insertError) {
79+
return { error: insertError };
80+
}
81+
82+
return { customerId: customer.id };
83+
} catch (e) {
84+
return { error: e };
85+
}
86+
} catch (error) {
87+
console.error("Error in getOrCreateCustomerId:", error);
88+
return { error };
89+
}
90+
}
91+
92+
/**
93+
* Fetches a user's subscription information from Stripe
94+
*/
95+
export async function fetchSubscription({
96+
customerId,
97+
}: {
98+
customerId: string;
99+
}): Promise<{
100+
primarySubscription?: {
101+
stripeSubscription: any;
102+
appSubscription: any;
103+
};
104+
hasEverHadSubscription: boolean;
105+
error?: any;
106+
}> {
107+
if (!isStripeConfigured()) {
108+
return {
109+
hasEverHadSubscription: false,
110+
error: "Stripe is not properly configured",
111+
};
112+
}
113+
114+
try {
115+
// Fetch user's subscriptions
116+
const stripeSubscriptions = await stripe.subscriptions.list({
117+
customer: customerId,
118+
limit: 100,
119+
status: "all",
120+
});
121+
122+
// Find "primary" subscription - an active one including trials and past_due in grace period
123+
const primaryStripeSubscription = stripeSubscriptions.data.find((sub) =>
124+
["active", "trialing", "past_due"].includes(sub.status),
125+
);
126+
127+
let appSubscription = null;
128+
if (primaryStripeSubscription) {
129+
const productId =
130+
primaryStripeSubscription.items?.data?.[0]?.price.product?.toString() ??
131+
"";
132+
133+
appSubscription = pricingPlans.find(
134+
(plan) => plan.stripe_product_id === productId,
135+
);
136+
137+
if (!appSubscription) {
138+
return {
139+
hasEverHadSubscription: stripeSubscriptions.data.length > 0,
140+
error:
141+
"Stripe subscription does not match any plan in pricing-plans.ts",
142+
};
143+
}
144+
}
145+
146+
let primarySubscription = null;
147+
if (primaryStripeSubscription && appSubscription) {
148+
primarySubscription = {
149+
stripeSubscription: primaryStripeSubscription,
150+
appSubscription,
151+
};
152+
}
153+
154+
return {
155+
primarySubscription,
156+
hasEverHadSubscription: stripeSubscriptions.data.length > 0,
157+
};
158+
} catch (error) {
159+
console.error("Error fetching subscription:", error);
160+
return {
161+
hasEverHadSubscription: false,
162+
error,
163+
};
164+
}
165+
}

0 commit comments

Comments
 (0)