feat(#29): Add example product tool requiring purchase to access

This commit is contained in:
Josh Creek
2024-09-22 12:06:39 +01:00
parent a4764c547e
commit fce4c0e0a5
11 changed files with 101 additions and 3 deletions
+1
View File
@@ -3,3 +3,4 @@ PUBLIC_SUPABASE_ANON_KEY=""
SUPABASE_SERVICE_ROLE_KEY="" SUPABASE_SERVICE_ROLE_KEY=""
STRIPE_SECRET_KEY="" STRIPE_SECRET_KEY=""
STRIPE_ENDPOINT_SECRET="" STRIPE_ENDPOINT_SECRET=""
VITE_PRODUCT_ID_EXAMPLEPRODUCT=""
+7
View File
@@ -6,6 +6,13 @@ Databases are stored in Supabase. The auth table for signing up and logging in u
To add the tables for recording their active subscriptions and bought products, as well as products and prices, you can use the SQL scripts from the `supabase/migrations` folder in the Supabase SQL Editor. To add the tables for recording their active subscriptions and bought products, as well as products and prices, you can use the SQL scripts from the `supabase/migrations` folder in the Supabase SQL Editor.
## Common commands for local development
- `npx supabase start` - Start the local Supabase instance
- `npx supabase stop` - Stop the local Supabase instance
- `npx supabase db reset` - Reset the local database
- `npx supabase gen types typescript --local > src/lib/types/supabase.d.ts` - Generate types for the local database
## Generating Types ## Generating Types
To get the types for the tables, you can follow these instructions taken from [here](https://supabase.com/docs/guides/api/rest/generating-types#generating-types-using-supabase-cli): To get the types for the tables, you can follow these instructions taken from [here](https://supabase.com/docs/guides/api/rest/generating-types#generating-types-using-supabase-cli):
+1 -1
View File
@@ -20,4 +20,4 @@
<li><a href="/">Submenu 2</a></li> <li><a href="/">Submenu 2</a></li>
</ul> </ul>
</li> </li>
<li><a href="/">Item 3</a></li> <li><a href="/tools/exampleproduct">Example Product</a></li>
+10
View File
@@ -201,6 +201,7 @@ export type Database = {
id: string id: string
metadata: Json | null metadata: Json | null
price_id: string | null price_id: string | null
product_id: string
quantity: number | null quantity: number | null
status: Database["public"]["Enums"]["subscription_status"] | null status: Database["public"]["Enums"]["subscription_status"] | null
trial_end: string | null trial_end: string | null
@@ -218,6 +219,7 @@ export type Database = {
id: string id: string
metadata?: Json | null metadata?: Json | null
price_id?: string | null price_id?: string | null
product_id: string
quantity?: number | null quantity?: number | null
status?: Database["public"]["Enums"]["subscription_status"] | null status?: Database["public"]["Enums"]["subscription_status"] | null
trial_end?: string | null trial_end?: string | null
@@ -235,6 +237,7 @@ export type Database = {
id?: string id?: string
metadata?: Json | null metadata?: Json | null
price_id?: string | null price_id?: string | null
product_id?: string
quantity?: number | null quantity?: number | null
status?: Database["public"]["Enums"]["subscription_status"] | null status?: Database["public"]["Enums"]["subscription_status"] | null
trial_end?: string | null trial_end?: string | null
@@ -249,6 +252,13 @@ export type Database = {
referencedRelation: "prices" referencedRelation: "prices"
referencedColumns: ["id"] referencedColumns: ["id"]
}, },
{
foreignKeyName: "subscriptions_product_id_fkey"
columns: ["product_id"]
isOneToOne: false
referencedRelation: "products"
referencedColumns: ["id"]
},
{ {
foreignKeyName: "subscriptions_user_id_fkey" foreignKeyName: "subscriptions_user_id_fkey"
columns: ["user_id"] columns: ["user_id"]
+36 -1
View File
@@ -211,6 +211,7 @@ const manageSubscriptionStatusChange = async (
user_id: uuid, user_id: uuid,
metadata: subscription.metadata, metadata: subscription.metadata,
status: subscription.status, status: subscription.status,
product_id: subscription.items.data[0].price.product as string,
price_id: subscription.items.data[0].price.id, price_id: subscription.items.data[0].price.id,
quantity: 1, //subscription.quantity, quantity: 1, //subscription.quantity,
cancel_at_period_end: subscription.cancel_at_period_end, cancel_at_period_end: subscription.cancel_at_period_end,
@@ -264,6 +265,39 @@ const recordProductPurchase = async (userId: string, productId: string, priceId:
console.log(`Product purchased recorded for user [${userId}] and product [${productId}]`); console.log(`Product purchased recorded for user [${userId}] and product [${productId}]`);
}; };
const hasProductAccess = async (userId: string, productId: string) => {
// Check if user has purchased the product
const { data: purchases, error: purchaseError } = await supabaseAdmin
.from('purchases')
.select('*')
.eq('user_id', userId)
.eq('product_id', productId);
if (purchaseError) throw new Error(purchaseError.message);
if (purchases && purchases.length > 0) {
// User has purchased the product
return true;
}
// Check if user has an active subscription for the product
const { data: subscriptions, error: subError } = await supabaseAdmin
.from('subscriptions')
.select('*')
.eq('user_id', userId)
.eq('product_id', productId)
.in('status', ['active', 'trialing']);
if (subError) throw new Error(subError.message);
if (subscriptions && subscriptions.length > 0) {
// User has an active subscription
return true;
}
return false;
};
export { export {
upsertProductRecord, upsertProductRecord,
upsertPriceRecord, upsertPriceRecord,
@@ -271,5 +305,6 @@ export {
deletePriceRecord, deletePriceRecord,
createOrRetrieveCustomer, createOrRetrieveCustomer,
manageSubscriptionStatusChange, manageSubscriptionStatusChange,
recordProductPurchase recordProductPurchase,
hasProductAccess
}; };
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('./$types').PageLoad} */
import { redirect } from '@sveltejs/kit';
export const load = async ({ params, locals: { safeGetSession } }) => {
const { session } = await safeGetSession();
if (!session) {
throw redirect(303, '/login'); // Redirect unauthenticated users to login
}
return params;
};
+9
View File
@@ -0,0 +1,9 @@
<script lang="ts">
import MagicLink from '$lib/components/MagicLink.svelte';
export let data;
let { supabase, session, url } = data;
$: ({ supabase, session, url } = data);
</script>
<MagicLink {supabase} {url} />
@@ -0,0 +1,19 @@
/** @type {import('./$types').PageLoad} */
import { redirect } from '@sveltejs/kit';
import { hasProductAccess } from '$lib/utils/supabase/admin';
import { VITE_PRODUCT_ID_EXAMPLEPRODUCT } from '$env/static/private'; // Import the example product ID from env
export const load = async ({ params, locals: { safeGetSession } }) => {
const { session } = await safeGetSession();
if (!session) {
throw redirect(303, '/login'); // Redirect unauthenticated users to login
}
// Check if the user has access to the 'EXAMPLE PRODUCT' product
const accessGranted = await hasProductAccess(session.user.id, VITE_PRODUCT_ID_EXAMPLEPRODUCT);
if (!accessGranted) {
throw redirect(303, '/unauthorised');
}
return params;
};
@@ -0,0 +1,2 @@
<h1>Example Product</h1>
<p>This is an example of a tool you could be selling.</p>
+2
View File
@@ -0,0 +1,2 @@
<h1>Unauthorised Access</h1>
<p>Sorry, you don't have permission to access this tool.</p>
@@ -110,6 +110,8 @@ create table subscriptions (
status subscription_status, status subscription_status,
-- Set of key-value pairs, used to store additional information about the object in a structured format. -- Set of key-value pairs, used to store additional information about the object in a structured format.
metadata jsonb, metadata jsonb,
-- ID of the product this subscription is for.
product_id text references products(id) not null,
-- ID of the price that created this subscription. -- ID of the price that created this subscription.
price_id text references prices, price_id text references prices,
-- Quantity multiplied by the unit amount of the price creates the amount of the subscription. Can be used to charge multiple seats. -- Quantity multiplied by the unit amount of the price creates the amount of the subscription. Can be used to charge multiple seats.