mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-13 11:03:49 +00:00
feat(#59): Enable showing subscriptions and transactions
This commit is contained in:
@@ -480,6 +480,96 @@ const getProductById = async (productId: string) => {
|
|||||||
return product as Product;
|
return product as Product;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUserSubscriptions = async (userId: string) => {
|
||||||
|
try {
|
||||||
|
// Step 1: Retrieve subscription data
|
||||||
|
const { data: subscriptions, error } = await supabaseAdmin
|
||||||
|
.from('subscriptions')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error retrieving subscriptions:', error.message);
|
||||||
|
throw new Error('Failed to fetch subscriptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Map through each subscription and fetch product and price details
|
||||||
|
const subscriptionDetails = await Promise.all(
|
||||||
|
subscriptions.map(async (sub) => {
|
||||||
|
// Fetch product and price details from Stripe
|
||||||
|
const product = await stripeClient.products.retrieve(sub.product_id);
|
||||||
|
const price = await stripeClient.prices.retrieve(sub.price_id);
|
||||||
|
|
||||||
|
console.log(sub);
|
||||||
|
|
||||||
|
const expiryDate = new Date(sub.current_period_end);
|
||||||
|
|
||||||
|
// Format output data
|
||||||
|
return {
|
||||||
|
productName: product.name,
|
||||||
|
amount: (price.unit_amount / 100).toFixed(2),
|
||||||
|
currency: price.currency.toUpperCase(),
|
||||||
|
interval: price.recurring?.interval || 'one-time',
|
||||||
|
expiryDate: expiryDate.toLocaleDateString('en-GB', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
}),
|
||||||
|
status: sub.status
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return subscriptionDetails;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('An error occurred while retrieving subscription details:', error);
|
||||||
|
throw new Error('Could not retrieve subscription details');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserTransactions = async (userId: string) => {
|
||||||
|
try {
|
||||||
|
// Step 1: Retrieve the customer ID associated with the user
|
||||||
|
const { data, error } = await supabaseAdmin
|
||||||
|
.from('customers')
|
||||||
|
.select('stripe_customer_id')
|
||||||
|
.eq('id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error retrieving user data:', error.message);
|
||||||
|
throw new Error('Failed to fetch user data');
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerId = data?.stripe_customer_id;
|
||||||
|
if (!customerId) {
|
||||||
|
throw new Error('No Stripe customer ID found for this user');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Fetch charge transactions from Stripe
|
||||||
|
const charges = await stripeClient.charges.list({ customer: customerId });
|
||||||
|
|
||||||
|
// Step 3: Format transaction data
|
||||||
|
const transactions = charges.data.map((charge) => ({
|
||||||
|
amount: (charge.amount / 100).toFixed(2),
|
||||||
|
currency: charge.currency.toUpperCase(),
|
||||||
|
description: charge.description ?? 'No description provided',
|
||||||
|
status: charge.status,
|
||||||
|
created: new Date(charge.created * 1000).toLocaleDateString('en-GB', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
}),
|
||||||
|
receipt_url: charge.receipt_url
|
||||||
|
}));
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('An error occurred while retrieving transactions:', error);
|
||||||
|
throw new Error('Could not retrieve transactions');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
upsertProductRecord,
|
upsertProductRecord,
|
||||||
upsertPriceRecord,
|
upsertPriceRecord,
|
||||||
@@ -495,5 +585,7 @@ export {
|
|||||||
getActiveProductsWithPrices,
|
getActiveProductsWithPrices,
|
||||||
getProductById,
|
getProductById,
|
||||||
upsertCustomerToSupabase,
|
upsertCustomerToSupabase,
|
||||||
getStripeCustomerId
|
getStripeCustomerId,
|
||||||
|
getUserSubscriptions,
|
||||||
|
getUserTransactions
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { fail, redirect } from '@sveltejs/kit';
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
import { getUserSubscriptions, getUserTransactions } from '$lib/utils/supabase/admin';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
|
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
|
||||||
const { session } = await safeGetSession();
|
const { session } = await safeGetSession();
|
||||||
@@ -14,7 +15,11 @@ export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession
|
|||||||
.eq('id', session.user.id)
|
.eq('id', session.user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
return { session, user };
|
const subscriptions = await getUserSubscriptions(session.user.id);
|
||||||
|
|
||||||
|
const transactions = await getUserTransactions(session.user.id);
|
||||||
|
|
||||||
|
return { session, user, subscriptions, transactions };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
@@ -27,20 +32,23 @@ export const actions: Actions = {
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
return fail(401, { name });
|
return fail(401, { name });
|
||||||
}
|
}
|
||||||
const { error } = await supabase.from('users').update({
|
const { error } = await supabase
|
||||||
name: name,
|
.from('users')
|
||||||
}).eq('id', session?.user.id);
|
.update({
|
||||||
|
name: name
|
||||||
|
})
|
||||||
|
.eq('id', session?.user.id);
|
||||||
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
name,
|
name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
signout: async ({ locals: { supabase, safeGetSession } }) => {
|
signout: async ({ locals: { supabase, safeGetSession } }) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
export let data;
|
export let data;
|
||||||
export let form;
|
export let form;
|
||||||
|
|
||||||
let { session, supabase, user } = data;
|
let { session, user, subscriptions, transactions } = data;
|
||||||
$: ({ session, supabase, user } = data);
|
$: ({ session, supabase, user } = data);
|
||||||
|
|
||||||
let profileForm: HTMLFormElement;
|
let profileForm: HTMLFormElement;
|
||||||
@@ -18,16 +18,6 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const subscriptions = [
|
|
||||||
{ name: 'Premium Plan', nextBillingDate: '12th August 2021' },
|
|
||||||
{ name: 'Basic Plan', nextBillingDate: '12th August 2021' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const billingHistory = [
|
|
||||||
{ id: 123, date: '12th August 2021', amount: 9.99, downloadLink: '#' },
|
|
||||||
{ id: 122, date: '12th July 2021', amount: 9.99, downloadLink: '#' }
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -35,7 +25,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Flex container for two columns -->
|
<!-- Flex container for two columns -->
|
||||||
<div class="flex flex-col lg:flex-row gap-6">
|
<div class="flex flex-col lg:flex-row gap-6">
|
||||||
<!-- Left Column: Account Overview, Active Subscriptions, and Billing History -->
|
<!-- Left Column: Account Overview, Subscriptions, and Billing History -->
|
||||||
<div class="flex-1 space-y-6">
|
<div class="flex-1 space-y-6">
|
||||||
<!-- Account Overview -->
|
<!-- Account Overview -->
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
@@ -83,35 +73,50 @@
|
|||||||
|
|
||||||
<!-- Active Subscriptions -->
|
<!-- Active Subscriptions -->
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<h2 class="text-xl font-semibold">Active Subscriptions</h2>
|
<h2 class="text-xl font-semibold">Subscriptions</h2>
|
||||||
<ul class="space-y-3">
|
{#if subscriptions.length === 0}
|
||||||
{#each subscriptions as subscription}
|
<p class="text-gray-600">You do not have any subscriptions.</p>
|
||||||
<li class="p-4 border border-gray-200 rounded-md">
|
{:else}
|
||||||
<div class="flex items-center justify-between">
|
<ul class="space-y-3">
|
||||||
<div>
|
{#each subscriptions as subscription}
|
||||||
<p class="font-semibold">{subscription.name}</p>
|
<li class="p-4 border border-gray-200 rounded-md">
|
||||||
<p class="text-sm text-gray-500">Renewal Date: {subscription.nextBillingDate}</p>
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold">{subscription.productName}</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Price: {subscription.amount}
|
||||||
|
{subscription.currency} / {subscription.interval}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Expiry Date: {subscription.expiryDate}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Status: {subscription.status}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- <button class="btn btn-secondary btn-sm">Manage</button> -->
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-sm">Manage</button>
|
</li>
|
||||||
</div>
|
{/each}
|
||||||
</li>
|
</ul>
|
||||||
{/each}
|
{/if}
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Billing History -->
|
<!-- Billing History -->
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<h2 class="text-xl font-semibold">Billing History</h2>
|
<h2 class="text-xl font-semibold">Billing History</h2>
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
{#each billingHistory as invoice}
|
{#each transactions as transaction}
|
||||||
<li class="flex justify-between p-4 border border-gray-200 rounded-md">
|
<li class="flex justify-between p-4 border border-gray-200 rounded-md">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold">Invoice #{invoice.id}</p>
|
<p class="text-sm text-gray-500">Date: {transaction.created}</p>
|
||||||
<p class="text-sm text-gray-500">Date: {invoice.date}</p>
|
<p class="text-sm text-gray-500">
|
||||||
<p class="text-sm text-gray-500">Amount: £{invoice.amount}</p>
|
Amount: {transaction.amount}
|
||||||
|
{transaction.currency}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a href={invoice.downloadLink} class="btn btn-outline btn-sm" target="_blank"
|
<a href={transaction.receipt_url} class="btn btn-outline btn-sm" target="_blank"
|
||||||
>Download</a
|
>Download Receipt</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user