diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 514a63f..2e94248 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -76,6 +76,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { active: item.active, invoiceId: item.invoiceId, invoiceAmountUsd: item.amount, + createdAt: item.createdAt || null, }), ); reply.status(200).send(parsed); @@ -131,6 +132,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { url, amount: request.body.invoiceAmountUsd, active: true, + createdAt: new Date().toISOString(), }), }); await fastify.dynamoClient.send(dynamoCommand); diff --git a/src/common/config.ts b/src/common/config.ts index 90465bc..736a924 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,4 +1,4 @@ -import { allAppRoles, AppRoles, RunEnvironment } from "./roles.js"; +import { AppRoles, RunEnvironment } from "./roles.js"; import { OriginFunction } from "@fastify/cors"; // From @fastify/cors @@ -75,9 +75,11 @@ const environmentConfig: EnvironmentConfigType = { AadValidClientId: "39c28870-94e4-47ee-b4fb-affe0bf96c9f", PasskitIdentifier: "pass.org.acmuiuc.qa.membership", PasskitSerialNumber: "0", - MembershipApiEndpoint: "https://infra-membership-api.aws.qa.acmuiuc.org/api/v1/checkMembership", + MembershipApiEndpoint: + "https://infra-membership-api.aws.qa.acmuiuc.org/api/v1/checkMembership", EmailDomain: "aws.qa.acmuiuc.org", - SqsQueueUrl: "https://sqs.us-east-1.amazonaws.com/427040638965/infra-core-api-sqs" + SqsQueueUrl: + "https://sqs.us-east-1.amazonaws.com/427040638965/infra-core-api-sqs", }, prod: { AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] }, @@ -90,10 +92,12 @@ const environmentConfig: EnvironmentConfigType = { AadValidClientId: "5e08cf0f-53bb-4e09-9df2-e9bdc3467296", PasskitIdentifier: "pass.edu.illinois.acm.membership", PasskitSerialNumber: "0", - MembershipApiEndpoint: "https://infra-membership-api.aws.acmuiuc.org/api/v1/checkMembership", + MembershipApiEndpoint: + "https://infra-membership-api.aws.acmuiuc.org/api/v1/checkMembership", EmailDomain: "acm.illinois.edu", - SqsQueueUrl: "https://sqs.us-east-1.amazonaws.com/298118738376/infra-core-api-sqs" - } + SqsQueueUrl: + "https://sqs.us-east-1.amazonaws.com/298118738376/infra-core-api-sqs", + }, }; export type SecretConfig = { diff --git a/src/common/types/stripe.ts b/src/common/types/stripe.ts index 603b4a2..1ca18db 100644 --- a/src/common/types/stripe.ts +++ b/src/common/types/stripe.ts @@ -28,6 +28,7 @@ export const invoiceLinkGetResponseSchema = z.array( active: z.boolean(), invoiceId: z.string().min(1), invoiceAmountUsd: z.number().min(50), + createdAt: z.union([z.string().date(), z.null()]), }), ); diff --git a/src/ui/pages/stripe/CreateLink.tsx b/src/ui/pages/stripe/CreateLink.tsx index 0aa5195..8b61289 100644 --- a/src/ui/pages/stripe/CreateLink.tsx +++ b/src/ui/pages/stripe/CreateLink.tsx @@ -22,14 +22,11 @@ import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen'; interface StripeCreateLinkPanelProps { createLink: (payload: PostInvoiceLinkRequest) => Promise; - isLoading: boolean; } -export const StripeCreateLinkPanel: React.FC = ({ - createLink, - isLoading, -}) => { +export const StripeCreateLinkPanel: React.FC = ({ createLink }) => { const [modalOpened, setModalOpened] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [returnedLink, setReturnedLink] = useState(null); const form = useForm({ @@ -49,17 +46,21 @@ export const StripeCreateLinkPanel: React.FC = ({ const handleSubmit = async (values: typeof form.values) => { try { + setIsLoading(true); const response = await createLink(values); setReturnedLink(response.link); + setIsLoading(false); setModalOpened(true); form.reset(); - } catch (err) { + } catch (e) { + setIsLoading(false); notifications.show({ title: 'Error', message: 'Failed to create payment link. Please try again or contact support.', color: 'red', icon: , }); + throw e; } }; diff --git a/src/ui/pages/stripe/CurrentLinks.tsx b/src/ui/pages/stripe/CurrentLinks.tsx index e6b716d..20a5783 100644 --- a/src/ui/pages/stripe/CurrentLinks.tsx +++ b/src/ui/pages/stripe/CurrentLinks.tsx @@ -1,24 +1,162 @@ -import { Box, Card, Divider, Text, Title } from '@mantine/core'; -import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react'; -import React, { useState } from 'react'; +import { + Badge, + Box, + Button, + Card, + Checkbox, + CopyButton, + Divider, + Group, + Loader, + NumberFormatter, + Table, + Text, + Title, +} from '@mantine/core'; +import { IconAlertCircle, IconAlertTriangle, IconCircleCheck } from '@tabler/icons-react'; +import React, { useEffect, useState } from 'react'; import { GetInvoiceLinksResponse } from '@common/types/stripe'; import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen'; +import { notifications } from '@mantine/notifications'; +import { useAuth } from '@ui/components/AuthContext'; +import pluralize from 'pluralize'; interface StripeCurrentLinksPanelProps { - links: GetInvoiceLinksResponse; - isLoading: boolean; + getLinks: () => Promise; } -export const StripeCurrentLinksPanel: React.FC = ({ - links, - isLoading, -}) => { +export const StripeCurrentLinksPanel: React.FC = ({ getLinks }) => { + const [links, setLinks] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [selectedRows, setSelectedRows] = useState([]); + const { userData } = useAuth(); + useEffect(() => { + const getLinksOnLoad = async () => { + try { + setIsLoading(true); + const data = await getLinks(); + setLinks(data); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + notifications.show({ + title: 'Error', + message: 'Failed to get payment links. Please try again or contact support.', + color: 'red', + icon: , + }); + } + }; + getLinksOnLoad(); + }, []); + const createTableRow = (data: GetInvoiceLinksResponse[number]) => { + return ( + + + + setSelectedRows( + event.currentTarget.checked + ? [...selectedRows, data.id] + : selectedRows.filter((id) => id !== data.id) + ) + } + /> + + + {data.active && ( + + Active + + )} + {!data.active && ( + + Inactive + + )} + + {data.invoiceId} + + + + {data.userId.replace(userData!.email!, 'You')} + {data.createdAt === null ? 'Unknown' : data.createdAt} + + + {({ copied, copy }) => ( + + )} + + + + ); + }; + const deactivateLinks = (linkIds: string[]) => { + notifications.show({ + title: 'Feature not available', + message: 'Coming soon!', + color: 'yellow', + icon: , + }); + }; + return (
- - Current Links - - Coming soon! + + + Current Links + + {selectedRows.length > 0 && ( + + )} + + + {isLoading && } + {!isLoading && links && ( + + + + + + + setSelectedRows(() => { + if (selectedRows.length === links.length) { + return []; + } + return links.map((x) => x.id); + }) + } + /> + + Status + Invoice ID + Invoice Amount + Created By + Created At + Payment Link + + + {links.map(createTableRow)} +
+
+ )}
); }; diff --git a/src/ui/pages/stripe/ViewLinks.page.tsx b/src/ui/pages/stripe/ViewLinks.page.tsx index 4114346..b8a09dd 100644 --- a/src/ui/pages/stripe/ViewLinks.page.tsx +++ b/src/ui/pages/stripe/ViewLinks.page.tsx @@ -4,24 +4,25 @@ import { AuthGuard } from '@ui/components/AuthGuard'; import { AppRoles } from '@common/roles'; import StripeCurrentLinksPanel from './CurrentLinks'; import StripeCreateLinkPanel from './CreateLink'; -import { PostInvoiceLinkRequest, PostInvoiceLinkResponse } from '@common/types/stripe'; +import { + GetInvoiceLinksResponse, + PostInvoiceLinkRequest, + PostInvoiceLinkResponse, +} from '@common/types/stripe'; import { useApi } from '@ui/util/api'; export const ManageStripeLinksPage: React.FC = () => { - const [isLoading, setIsLoading] = useState(false); const api = useApi('core'); const createLink = async (payload: PostInvoiceLinkRequest): Promise => { const modifiedPayload = { ...payload, invoiceAmountUsd: payload.invoiceAmountUsd * 100 }; - try { - setIsLoading(true); - const response = await api.post('/api/v1/stripe/paymentLinks', modifiedPayload); - setIsLoading(false); - return response.data; - } catch (e) { - setIsLoading(false); - throw e; - } + const response = await api.post('/api/v1/stripe/paymentLinks', modifiedPayload); + return response.data; + }; + + const getLinks = async (): Promise => { + const response = await api.get('/api/v1/stripe/paymentLinks'); + return response.data; }; return ( @@ -32,11 +33,8 @@ export const ManageStripeLinksPage: React.FC = () => { Stripe Link Creator Create a Stripe Payment Link to accept credit card payments. - - + + );