From c77dc6ac47cc6c271ef8815578183b94427c25b7 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:04:02 +0000 Subject: [PATCH 1/5] feat(#76): Add pagination for stripe orders on account page --- src/lib/utils/supabase/admin.ts | 18 +++++++- src/routes/account/+page.server.ts | 33 +++++++++++++- src/routes/account/+page.svelte | 69 +++++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts index 659f3ed..06bb5b2 100644 --- a/src/lib/utils/supabase/admin.ts +++ b/src/lib/utils/supabase/admin.ts @@ -590,7 +590,12 @@ const getUserSubscriptions = async (userId: string) => { } }; -const getUserTransactions = async (userId: string) => { +const getUserTransactions = async ( + userId: string, + perPage: number, + startingAfter: string | undefined = undefined, + endingBefore: string | undefined = undefined +) => { try { // Step 1: Retrieve the customer ID associated with the user const { data, error } = await supabaseAdmin @@ -610,10 +615,19 @@ const getUserTransactions = async (userId: string) => { } // Step 2: Fetch charge transactions from Stripe - const charges = await stripeClient.charges.list({ customer: customerId }); + const params: stripe.ChargeListParams = { + customer: customerId, + limit: perPage + }; + + if (startingAfter) params.starting_after = startingAfter; + if (endingBefore) params.ending_before = endingBefore; + + const charges = await stripeClient.charges.list(params); // Step 3: Format transaction data const transactions = charges.data.map((charge) => ({ + id: charge.id, amount: (charge.amount / 100).toFixed(2), currency: charge.currency.toUpperCase(), description: charge.description ?? 'No description provided', diff --git a/src/routes/account/+page.server.ts b/src/routes/account/+page.server.ts index f03bd02..3bb27e6 100644 --- a/src/routes/account/+page.server.ts +++ b/src/routes/account/+page.server.ts @@ -4,6 +4,8 @@ import logger from '$lib/utils/logger/logger'; import { getUserSubscriptions, getUserTransactions } from '$lib/utils/supabase/admin'; import { requestAccountDeletion } from '$lib/utils/supabase/admin'; +const transactionsPerPage = 5; + export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => { const { session } = await safeGetSession(); @@ -19,9 +21,9 @@ export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession const subscriptions = await getUserSubscriptions(session.user.id); - const transactions = await getUserTransactions(session.user.id); + const transactions = await getUserTransactions(session.user.id, transactionsPerPage); - return { session, user, subscriptions, transactions }; + return { session, user, subscriptions, transactions, pageSize: transactionsPerPage }; }; export const actions: Actions = { @@ -78,5 +80,32 @@ export const actions: Actions = { return { name }; + }, + paginate: async ({ request, locals: { safeGetSession } }) => { + const formData = await request.formData(); + const pageSize = transactionsPerPage; + const startingAfter = formData.get('startingAfter')?.toString() || undefined; + const endingBefore = formData.get('endingBefore')?.toString() || undefined; + + const { session } = await safeGetSession(); + if (!session) return fail(401, { error: 'Not authenticated' }); + + try { + const transactions = await getUserTransactions( + session.user.id, + pageSize, + startingAfter, + endingBefore + ); + + const serializableTransactions = transactions.map((transaction) => ({ ...transaction })); + + const stringifiedTransactions: string = JSON.stringify(serializableTransactions); + + return { stringifiedTransactions }; + } catch (error) { + logger.error('Failed to fetch transactions', { userId: session.user.id, error }); + return fail(500, { error: 'Failed to load transactions' }); + } } }; diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index 1c1e4fb..0bf4dbe 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -1,12 +1,13 @@

Billing History

- + {:else} +

No transactions found.

+ {/if} + + +
+ + + +
From 5de84d2ec1e493a29527052a7a24e0479f3246af Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:09:01 +0000 Subject: [PATCH 2/5] feat(#76): Ensure pagination is only possible for valid pages --- src/lib/utils/supabase/admin.ts | 4 +- src/routes/account/+page.server.ts | 39 ++++++++++--- src/routes/account/+page.svelte | 94 +++++++++++++++--------------- 3 files changed, 81 insertions(+), 56 deletions(-) diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts index 06bb5b2..7252c76 100644 --- a/src/lib/utils/supabase/admin.ts +++ b/src/lib/utils/supabase/admin.ts @@ -625,6 +625,8 @@ const getUserTransactions = async ( const charges = await stripeClient.charges.list(params); + const hasNextPage = charges.has_more; + // Step 3: Format transaction data const transactions = charges.data.map((charge) => ({ id: charge.id, @@ -640,7 +642,7 @@ const getUserTransactions = async ( receipt_url: charge.receipt_url })); - return transactions; + return { transactions, hasNextPage }; } catch (error) { console.error('An error occurred while retrieving transactions:', error); throw new Error('Could not retrieve transactions'); diff --git a/src/routes/account/+page.server.ts b/src/routes/account/+page.server.ts index 3bb27e6..13681ed 100644 --- a/src/routes/account/+page.server.ts +++ b/src/routes/account/+page.server.ts @@ -5,6 +5,8 @@ import { getUserSubscriptions, getUserTransactions } from '$lib/utils/supabase/a import { requestAccountDeletion } from '$lib/utils/supabase/admin'; const transactionsPerPage = 5; +let firstTransactionId: string; +let hasMoreThanOnePage = false; export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => { const { session } = await safeGetSession(); @@ -21,9 +23,19 @@ export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession const subscriptions = await getUserSubscriptions(session.user.id); - const transactions = await getUserTransactions(session.user.id, transactionsPerPage); + const transactionsData = await getUserTransactions(session.user.id, transactionsPerPage); - return { session, user, subscriptions, transactions, pageSize: transactionsPerPage }; + const transactions = transactionsData.transactions; + firstTransactionId = transactions[0]?.id; + hasMoreThanOnePage = transactionsData.hasNextPage; + + return { + session, + user, + subscriptions, + transactions, + pageSize: transactionsPerPage + }; }; export const actions: Actions = { @@ -84,25 +96,34 @@ export const actions: Actions = { paginate: async ({ request, locals: { safeGetSession } }) => { const formData = await request.formData(); const pageSize = transactionsPerPage; - const startingAfter = formData.get('startingAfter')?.toString() || undefined; - const endingBefore = formData.get('endingBefore')?.toString() || undefined; + + let startingAfter = formData.get('startingAfter')?.toString() || undefined; + if (startingAfter === 'undefined') { + startingAfter = undefined; + } + let endingBefore = formData.get('endingBefore')?.toString() || undefined; + if (endingBefore === 'undefined') { + endingBefore = undefined; + } const { session } = await safeGetSession(); if (!session) return fail(401, { error: 'Not authenticated' }); try { - const transactions = await getUserTransactions( + const { transactions, hasNextPage } = await getUserTransactions( session.user.id, pageSize, startingAfter, endingBefore ); - const serializableTransactions = transactions.map((transaction) => ({ ...transaction })); + const isFirstPage = transactions[0].id === firstTransactionId; - const stringifiedTransactions: string = JSON.stringify(serializableTransactions); - - return { stringifiedTransactions }; + return { + transactions: JSON.stringify(transactions), + hasNextPage: hasNextPage || (isFirstPage && hasMoreThanOnePage), + isFirstPage + }; } catch (error) { logger.error('Failed to fetch transactions', { userId: session.user.id, error }); return fail(500, { error: 'Failed to load transactions' }); diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index 0bf4dbe..a4e6f5f 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -1,5 +1,4 @@
No transactions found.

{/if} - -
- + +
+
+ + - -
+ + + +
+
From 981917cd6f116c27c9231f643d665ce1b7553054 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:23:33 +0000 Subject: [PATCH 3/5] chore(#76): Remove unused variables --- src/routes/account/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index a4e6f5f..53d609e 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -5,8 +5,8 @@ export let data; export let form; - let { session, user, subscriptions, transactions, pageSize } = data; - $: ({ session, supabase, user, transactions } = data); + let { session, user, subscriptions, transactions } = data; + $: ({ session, user, transactions } = data); let profileForm: HTMLFormElement; let loading = false; From 5c5fc245b3e5200d465490af5a8053f816bc457f Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:25:26 +0000 Subject: [PATCH 4/5] feat(#76): Improve accessibility --- src/routes/account/+page.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index 53d609e..e9bae73 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -170,7 +170,11 @@
-
+