feat: memberships list page and fixes

This commit is contained in:
Artur N
2024-08-08 20:14:06 -03:00
parent f6ccdd6b18
commit 7b63490781
7 changed files with 160 additions and 48 deletions

View File

@@ -38,15 +38,9 @@ const Header = () => {
return (
<header className="flex items-center justify-between py-10">
<div>
<Link
href="/"
aria-label="Monero Fund"
className="flex items-center mr-3 gap-4"
>
<Link href="/" aria-label="Monero Fund" className="flex items-center mr-3 gap-4">
<Logo className="w-12 h-12" />
<span className="text-foreground text-lg font-bold">
MAGIC Monero Fund
</span>
<span className="text-foreground text-lg font-bold">MAGIC Monero Fund</span>
</Link>
</div>
<div className="flex gap-2 items-center text-base leading-5">
@@ -113,6 +107,14 @@ const Header = () => {
My Donations
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link
href="/account/my-memberships"
className="text-foreground hover:text-foreground"
>
My Memberships
</Link>
</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut({ callbackUrl: '/' })}>
Logout

View File

@@ -11,7 +11,6 @@ import {
} from '../../components/ui/table'
import { trpc } from '../../utils/trpc'
import Head from 'next/head'
import { Button } from '../../components/ui/button'
import CustomLink from '../../components/CustomLink'
dayjs.extend(localizedFormat)
@@ -31,53 +30,31 @@ function MyDonations() {
</Head>
<div className="w-full max-w-5xl mx-auto flex flex-col">
<div className="flex flex-row justify-between">
<h1 className="text-3xl font-bold mb-4">My Donations</h1>
{!!donationListQuery.data?.billingPortalUrl && (
<CustomLink href={donationListQuery.data.billingPortalUrl}>
Manage Subscriptions
</CustomLink>
)}
</div>
<h1 className="text-3xl font-bold mb-4">My Donations</h1>
<Table className="">
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Fund</TableHead>
<TableHead>Type</TableHead>
<TableHead>Method</TableHead>
<TableHead>Invoice ID</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Status</TableHead>
<TableHead>Date</TableHead>
<TableHead>Subscription End</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{donationListQuery.data?.donations.map((donation) => (
{donationListQuery.data?.map((donation) => (
<TableRow key={donation.createdAt.toISOString()}>
<TableCell className="font-medium">
{donation.projectName}
</TableCell>
<TableCell>{donation.fundName}</TableCell>
<TableCell>{donationTypePretty[donation.type]}</TableCell>
<TableCell>
{donation.method.charAt(0).toUpperCase() +
donation.method.slice(1)}
</TableCell>
<TableCell>
{donation.stripePaymentStatus ||
donation.stripeSubscriptionStatus ||
donation.btcPayStatus}
</TableCell>
<TableCell className="font-medium">{donation.projectName}</TableCell>
<TableCell>{donation.fund}</TableCell>
<TableCell>{donation.stripeInvoiceId ? 'Fiat' : 'Monero'}</TableCell>
<TableCell>{donation.stripeInvoiceId}</TableCell>
<TableCell>${donation.fiatAmount}</TableCell>
<TableCell>{donation.status}</TableCell>
<TableCell>{dayjs(donation.createdAt).format('lll')}</TableCell>
<TableCell>
{donation.subscriptionCancelAt
? dayjs(donation.subscriptionCancelAt).format('lll')
: '-'}
</TableCell>
<TableCell className="text-right">${donation.amount}</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -0,0 +1,71 @@
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../../components/ui/table'
import { trpc } from '../../utils/trpc'
import Head from 'next/head'
import CustomLink from '../../components/CustomLink'
dayjs.extend(localizedFormat)
function MyMemberships() {
const membershipListQuery = trpc.donation.membershipList.useQuery()
return (
<>
<Head>
<title>Monero Fund - My Memberships</title>
</Head>
<div className="w-full max-w-5xl mx-auto flex flex-col">
<div className="flex flex-row justify-between">
<h1 className="text-3xl font-bold mb-4">My Memberships</h1>
{membershipListQuery.data?.billingPortalUrl && (
<CustomLink
href={membershipListQuery.data?.billingPortalUrl}
aria-label="Manage Fiat Subscriptions"
>
Manage Fiat Subscriptions
</CustomLink>
)}
</div>
<Table className="">
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Fund</TableHead>
<TableHead>Method</TableHead>
<TableHead>Invoice ID</TableHead>
<TableHead>Status</TableHead>
<TableHead>Date</TableHead>
<TableHead>Period End</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{membershipListQuery.data?.memberships.map((membership) => (
<TableRow key={membership.createdAt.toISOString()}>
<TableCell className="font-medium">{membership.projectName}</TableCell>
<TableCell>{membership.fund}</TableCell>
<TableCell>{membership.stripeInvoiceId ? 'Fiat' : 'Monero'}</TableCell>
<TableCell>{membership.stripeInvoiceId || membership.btcPayInvoiceId}</TableCell>
<TableCell>{membership.status}</TableCell>
<TableCell>{dayjs(membership.createdAt).format('lll')}</TableCell>
<TableCell>{dayjs(membership.membershipExpiresAt).format('lll')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</>
)
}
export default MyMemberships

View File

@@ -3,6 +3,7 @@ import getRawBody from 'raw-body'
import { NextApiRequest, NextApiResponse } from 'next'
import { env } from '../../../env.mjs'
import { prisma, stripe } from '../../../server/services'
import { DonationMetadata } from '../../../server/types'
export const config = {
api: {
@@ -60,6 +61,36 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})
}
// Create subscription when subscription is created
if (event.type === 'customer.subscription.created') {
const subscription = event.data.object
const metadata = subscription.metadata as DonationMetadata
await prisma.donation.create({
data: {
userId: metadata.userId as string,
stripeInvoiceId: subscription.id,
projectName: metadata.projectName,
projectSlug: metadata.projectSlug,
fund: 'Monero Fund',
currency: 'USD',
fiatAmount: 100,
status: 'Complete',
membershipExpiresAt: new Date(subscription.current_period_end * 1000),
},
})
}
// Update subscription expiration date when subscription is updated
if (event.type === 'customer.subscription.updated') {
const subscription = event.data.object
await prisma.donation.updateMany({
where: { stripeInvoiceId: subscription.id },
data: { membershipExpiresAt: new Date(subscription.current_period_end * 1000) },
})
}
// Return a 200 response to acknowledge receipt of the event
res.status(200).end()
}

View File

@@ -12,7 +12,7 @@ CREATE TABLE "Donation" (
"projectSlug" TEXT NOT NULL,
"projectName" TEXT NOT NULL,
"fund" TEXT NOT NULL,
"crypto" TEXT NOT NULL,
"currency" TEXT NOT NULL,
"fiatAmount" DOUBLE PRECISION NOT NULL,
"membershipExpiresAt" TIMESTAMP(3),
"status" "DonationStatus" NOT NULL,
@@ -21,7 +21,10 @@ CREATE TABLE "Donation" (
);
-- CreateIndex
CREATE INDEX "Donation_btcPayInvoiceId_stripeInvoiceId_idx" ON "Donation"("btcPayInvoiceId", "stripeInvoiceId");
CREATE INDEX "Donation_btcPayInvoiceId_idx" ON "Donation"("btcPayInvoiceId");
-- CreateIndex
CREATE INDEX "Donation_stripeInvoiceId_idx" ON "Donation"("stripeInvoiceId");
-- CreateIndex
CREATE INDEX "Donation_userId_idx" ON "Donation"("userId");

View File

@@ -24,7 +24,7 @@ model Donation {
projectSlug String
projectName String
fund String
crypto String
currency String
fiatAmount Float
membershipExpiresAt DateTime?
status DonationStatus

View File

@@ -129,7 +129,7 @@ export const donationRouter = router({
data: {
userId: metadata.userId as string,
btcPayInvoiceId: response.data.id,
crypto: 'XMR',
currency: 'USD',
projectName: metadata.projectName,
projectSlug: metadata.projectSlug,
fund: 'Monero Fund',
@@ -270,6 +270,8 @@ export const donationRouter = router({
membershipExpiresAt: dayjs().add(1, 'year').toISOString(),
}
console.log(1)
const response = await btcpayApi.post(`/stores/${env.BTCPAY_STORE_ID}/invoices`, {
amount: MEMBERSHIP_PRICE,
currency: CURRENCY,
@@ -277,11 +279,13 @@ export const donationRouter = router({
checkout: { redirectURL: `${env.APP_URL}/thankyou` },
})
console.log(2)
await prisma.donation.create({
data: {
userId,
btcPayInvoiceId: response.data.id,
crypto: 'XMR',
currency: 'USD',
projectName: metadata.projectName,
projectSlug: metadata.projectSlug,
fund: 'Monero Fund',
@@ -295,9 +299,7 @@ export const donationRouter = router({
}),
donationList: protectedProcedure.query(async ({ ctx }) => {
await authenticateKeycloakClient()
const userId = ctx.session.user.sub
const user = await keycloak.users.findOne({ id: userId })
// Get all user's donations that are not expired OR are expired AND are less than 1 month old
const donations = await prisma.donation.findMany({
@@ -315,4 +317,30 @@ export const donationRouter = router({
return donations
}),
membershipList: protectedProcedure.query(async ({ ctx }) => {
await authenticateKeycloakClient()
const userId = ctx.session.user.sub
const user = await keycloak.users.findOne({ id: userId })
const stripeCustomerId = user?.attributes?.stripeCustomerId?.[0]
let billingPortalUrl: string | null = null
if (stripeCustomerId) {
const billingPortalSession = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: `${env.APP_URL}/account/my-memberships`,
})
billingPortalUrl = billingPortalSession.url
}
const memberships = await prisma.donation.findMany({
where: {
userId,
membershipExpiresAt: { not: null },
},
})
return { memberships, billingPortalUrl }
}),
})