Files
Rico Berger b8fdbf17a2 Rework Edge Functions (#247)
Instead of using an `import_map.json` file to define the versions for
dependencies, they are now defined directly within the import. Since the
`import_map.json` file should not be used anymore and instead a
`deno.json` file per function should be used, we decided to define them
directly with the code. The overhead compared to a `deno.json` file per
function shouldn't be that large and it makes using functions in a
self-hosted setup easier.
2025-04-23 17:00:21 +02:00

189 lines
5.2 KiB
TypeScript

import Stripe from "https://esm.sh/stripe@14.8.0?target=deno&no-check";
import { createClient } from "jsr:@supabase/supabase-js@2";
import { log } from "../utils/log.ts";
import {
FEEDDECK_STRIPE_API_KEY,
FEEDDECK_STRIPE_PRICE_ID,
FEEDDECK_SUPABASE_SERVICE_ROLE_KEY,
FEEDDECK_SUPABASE_SITE_URL,
FEEDDECK_SUPABASE_URL,
} from "../utils/constants.ts";
/**
* Create a new Stripe client, based on the `FEEDDECK_STRIPE_API_KEY`
* environment variable.
*/
export const stripe = new Stripe(FEEDDECK_STRIPE_API_KEY, {
apiVersion: "2022-11-15",
httpClient: Stripe.createFetchHttpClient(),
});
export const cryptoProvider = Stripe.createSubtleCryptoProvider();
/**
* `createOrRetrieveCustomer` returns the Stripe customer id for the provided
* `userId`. If the user doesn't have a Stripe customer id yet, a new customer
* is created for the user and the customer id is stored in the database before
* it is returned.
*/
export const createOrRetrieveCustomer = async (
userId: string,
userEmail?: string,
): Promise<string> => {
/**
* Create a new admin client for Supabase, which is used in the following
* steps to access the database. This client is required because the user
* client does not have the permissions to access the `profiles` table.
*/
const adminSupabaseClient = createClient(
FEEDDECK_SUPABASE_URL,
FEEDDECK_SUPABASE_SERVICE_ROLE_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
},
);
/**
* Get the user profile from the database. If there is no profile or more than
* one profile, we return an error.
*/
const { data: profile, error: profileError } = await adminSupabaseClient
.from("profiles")
.select("*")
.eq("id", userId);
if (profileError || profile?.length !== 1) {
log("error", "Failed to get user profile", {
user: userId,
error: profileError,
});
throw new Error("Failed to get user profile");
}
if (profile[0].stripeCustomerId) {
return profile[0].stripeCustomerId;
}
const customer = await stripe.customers.create({
email: userEmail,
metadata: {
userId: userId,
},
});
const { error: updateError } = await adminSupabaseClient
.from("profiles")
.update({
stripeCustomerId: customer.id,
})
.eq("id", userId);
if (updateError) {
log("error", "Failed to update user profile with Stripe customer id", {
stripeCustomerId: customer.id,
user: userId,
error: updateError,
});
throw new Error("Failed to update user profile with Stripe customer id");
}
return customer.id;
};
/**
* `createBillingPortalSession` creates a new session for the billing portal for
* the provided `stripeCustomerId`. The url which can be used by the user to
* open the billing portal is returned.
*/
export const createBillingPortalSession = async (
stripeCustomerId: string,
): Promise<string> => {
const { url } = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: FEEDDECK_SUPABASE_SITE_URL,
});
return url;
};
/**
* `createCheckoutSession` creates a new checkout session for the provided
* `stripeCustomerId`. The url which can be used by the user to open the
* checkout page is returned.
*/
export const createCheckoutSession = async (stripeCustomerId: string) => {
const { url } = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
customer_update: {
address: "auto",
},
line_items: [
{
price: FEEDDECK_STRIPE_PRICE_ID,
quantity: 1,
},
],
mode: "subscription",
allow_promotion_codes: true,
success_url: FEEDDECK_SUPABASE_SITE_URL,
cancel_url: FEEDDECK_SUPABASE_SITE_URL,
});
return url;
};
export const manageSubscriptionStatusChange = async (
stripeCustomerId: string,
isCreated = false,
) => {
/**
* Create a new admin client for Supabase, which is used in the following
* steps to access the database. This client is required because the user
* client does not have the permissions to access the `profiles` table.
*/
const adminSupabaseClient = createClient(
FEEDDECK_SUPABASE_URL,
FEEDDECK_SUPABASE_SERVICE_ROLE_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
},
);
/**
* Get the user profile from the database. If there is no profile or more than
* one profile, we return an error.
*/
const { data: profile, error: profileError } = await adminSupabaseClient
.from("profiles")
.select("*")
.eq("stripeCustomerId", stripeCustomerId);
if (profileError || profile?.length !== 1) {
log("error", "Failed to get user profile", {
stripeCustomerId: stripeCustomerId,
error: profileError,
});
throw new Error("Failed to get user profile");
}
const { error: updateError } = await adminSupabaseClient
.from("profiles")
.update({
tier: isCreated ? "premium" : "free",
subscriptionProvider: "stripe",
})
.eq("id", profile[0].id);
if (updateError) {
log("error", "Failed to update user profile with new tier value", {
stripeCustomerId: stripeCustomerId,
user: profile[0].id,
error: updateError,
});
throw new Error("Failed to update user profile with new tier value");
}
};